mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1020 from omnivore-app/fix/email-login-pages
Add confirm email landing page
This commit is contained in:
commit
d5cc05e787
21 changed files with 440 additions and 253 deletions
|
|
@ -19,7 +19,6 @@ import express from 'express'
|
|||
import axios from 'axios'
|
||||
import { env } from '../../env'
|
||||
import url from 'url'
|
||||
import { IntercomClient } from '../../utils/intercom'
|
||||
import { kx } from '../../datalayer/knex_config'
|
||||
import UserModel from '../../datalayer/user'
|
||||
import { buildLogger } from '../../utils/logger'
|
||||
|
|
@ -45,13 +44,15 @@ import {
|
|||
} from '../../utils/auth'
|
||||
import { createUser } from '../../services/create_user'
|
||||
import { isErrorWithCode } from '../../resolvers'
|
||||
import { initModels } from '../../server'
|
||||
import { getRepository } from '../../entity/utils'
|
||||
import { AppDataSource, initModels } from '../../server'
|
||||
import { getRepository, setClaims } from '../../entity/utils'
|
||||
import { User } from '../../entity/user'
|
||||
import {
|
||||
sendConfirmationEmail,
|
||||
sendPasswordResetEmail,
|
||||
} from '../../services/send_emails'
|
||||
import { createWebAuthToken } from './jwt_helpers'
|
||||
import { createSsoToken, ssoRedirectURL } from '../../utils/sso'
|
||||
|
||||
const logger = buildLogger('app.dispatch')
|
||||
const signToken = promisify(jwt.sign)
|
||||
|
|
@ -311,53 +312,52 @@ export function authRouter() {
|
|||
newUser: boolean
|
||||
): Promise<void> {
|
||||
try {
|
||||
const redirect = (res: express.Response): void => {
|
||||
let redirectUri: string | null = null
|
||||
if (req.query.state) {
|
||||
// Google login case: redirect_uri is in query state param.
|
||||
try {
|
||||
const state = JSON.parse((req.query?.state || '') as string)
|
||||
redirectUri = state?.redirect_uri
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'handleSuccessfulLogin: failed to parse redirect query state param',
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const message = res.get('Message')
|
||||
if (message) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/home?message=${encodeURIComponent(message)}`
|
||||
let redirectUri: string | null = null
|
||||
if (req.query.state) {
|
||||
// Google login case: redirect_uri is in query state param.
|
||||
try {
|
||||
const state = JSON.parse((req.query?.state || '') as string)
|
||||
redirectUri = state?.redirect_uri
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'handleSuccessfulLogin: failed to parse redirect query state param',
|
||||
err
|
||||
)
|
||||
}
|
||||
|
||||
if (newUser) {
|
||||
if (redirectUri && redirectUri !== '/') {
|
||||
return res.redirect(
|
||||
url.resolve(env.client.url, decodeURIComponent(redirectUri))
|
||||
)
|
||||
}
|
||||
return res.redirect(
|
||||
`${env.client.url}/settings/installation/extensions`
|
||||
)
|
||||
}
|
||||
|
||||
return res.redirect(
|
||||
url.resolve(env.client.url, decodeURIComponent(redirectUri || 'home'))
|
||||
)
|
||||
}
|
||||
|
||||
if (env.server.apiEnv && !env.dev.isLocal && IntercomClient) {
|
||||
if (newUser) {
|
||||
redirect(res)
|
||||
if (newUser) {
|
||||
if (redirectUri && redirectUri !== '/') {
|
||||
redirectUri = url.resolve(
|
||||
env.client.url,
|
||||
decodeURIComponent(redirectUri)
|
||||
)
|
||||
} else {
|
||||
redirect(res)
|
||||
redirectUri = `${env.client.url}/home`
|
||||
}
|
||||
} else {
|
||||
redirect(res)
|
||||
}
|
||||
|
||||
redirectUri = redirectUri ? redirectUri : `${env.client.url}/home`
|
||||
|
||||
const message = res.get('Message')
|
||||
if (message) {
|
||||
const u = new URL(redirectUri)
|
||||
u.searchParams.append('message', message)
|
||||
redirectUri = u.toString()
|
||||
}
|
||||
|
||||
// If we do have an auth token, we want to try redirecting to the
|
||||
// sso endpoint which will set a cookie for the client domain (omnivore.app)
|
||||
// after we set a cookie for the API domain (api-prod.omnivore.app)
|
||||
const authToken = await createWebAuthToken(user.id)
|
||||
if (authToken) {
|
||||
const ssoToken = createSsoToken(authToken, redirectUri)
|
||||
redirectUri = ssoRedirectURL(ssoToken)
|
||||
}
|
||||
|
||||
await setAuthInCookie({ uid: user.id }, res)
|
||||
|
||||
return res.redirect(redirectUri)
|
||||
} catch (error) {
|
||||
logger.info('handleSuccessfulLogin exception:', error)
|
||||
return res.redirect(`${env.client.url}/login?errorCodes=AUTH_FAILED`)
|
||||
|
|
@ -377,7 +377,7 @@ export function authRouter() {
|
|||
|
||||
if (!email || !password) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/email-login?errorCodes=${LoginErrorCode.InvalidCredentials}`
|
||||
`${env.client.url}/auth/email-login?errorCodes=${LoginErrorCode.InvalidCredentials}`
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -388,7 +388,7 @@ export function authRouter() {
|
|||
})
|
||||
if (!user?.id) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/email-login?errorCodes=${LoginErrorCode.UserNotFound}`
|
||||
`${env.client.url}/auth/email-login?errorCodes=${LoginErrorCode.UserNotFound}`
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -399,14 +399,14 @@ export function authRouter() {
|
|||
name: user.name,
|
||||
})
|
||||
return res.redirect(
|
||||
`${env.client.url}/email-login?errorCodes=PENDING_VERIFICATION`
|
||||
`${env.client.url}/auth/email-login?errorCodes=PENDING_VERIFICATION`
|
||||
)
|
||||
}
|
||||
|
||||
if (!user?.password) {
|
||||
// user has no password, so they need to set one
|
||||
return res.redirect(
|
||||
`${env.client.url}/email-login?errorCodes=${LoginErrorCode.WrongSource}`
|
||||
`${env.client.url}/auth/email-login?errorCodes=${LoginErrorCode.WrongSource}`
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -414,16 +414,16 @@ export function authRouter() {
|
|||
const validPassword = await comparePassword(password, user.password)
|
||||
if (!validPassword) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/email-login?errorCodes=${LoginErrorCode.InvalidCredentials}`
|
||||
`${env.client.url}/auth/email-login?errorCodes=${LoginErrorCode.InvalidCredentials}`
|
||||
)
|
||||
}
|
||||
|
||||
// set auth cookie in response header
|
||||
await setAuthInCookie({ uid: user.id }, res)
|
||||
await handleSuccessfulLogin(req, res, user, false)
|
||||
} catch (e) {
|
||||
logger.info('email-login exception:', e)
|
||||
res.redirect(`${env.client.url}/email-login?errorCodes=AUTH_FAILED`)
|
||||
res.redirect(
|
||||
`${env.client.url}/auth/email-login?errorCodes=AUTH_FAILED`
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -441,7 +441,7 @@ export function authRouter() {
|
|||
|
||||
if (!email || !password || !name || !username) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/email-signup?errorCodes=INVALID_CREDENTIALS`
|
||||
`${env.client.url}/auth/email-signup?errorCodes=INVALID_CREDENTIALS`
|
||||
)
|
||||
}
|
||||
const lowerCasedUsername = username.toLowerCase()
|
||||
|
|
@ -462,15 +462,17 @@ export function authRouter() {
|
|||
pendingConfirmation: true,
|
||||
})
|
||||
|
||||
res.redirect(`${env.client.url}/email-login?message=SIGNUP_SUCCESS`)
|
||||
res.redirect(
|
||||
`${env.client.url}/auth/verify-email?message=SIGNUP_SUCCESS`
|
||||
)
|
||||
} catch (e) {
|
||||
logger.info('email-signup exception:', e)
|
||||
if (isErrorWithCode(e)) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/email-signup?errorCodes=${e.errorCode}`
|
||||
`${env.client.url}/auth/email-signup?errorCodes=${e.errorCode}`
|
||||
)
|
||||
}
|
||||
res.redirect(`${env.client.url}/email-signup?errorCodes=UNKNOWN`)
|
||||
res.redirect(`${env.client.url}/auth/email-signup?errorCodes=UNKNOWN`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -491,36 +493,47 @@ export function authRouter() {
|
|||
const claims = await getClaimsByToken(token)
|
||||
if (!claims) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/confirm-email?errorCodes=INVALID_TOKEN`
|
||||
`${env.client.url}/auth/confirm-email?errorCodes=INVALID_TOKEN`
|
||||
)
|
||||
}
|
||||
|
||||
const user = await getRepository(User).findOneBy({ id: claims.uid })
|
||||
if (!user) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/confirm-email?errorCodes=USER_NOT_FOUND`
|
||||
`${env.client.url}/auth/confirm-email?errorCodes=USER_NOT_FOUND`
|
||||
)
|
||||
}
|
||||
|
||||
if (user.status === StatusType.Pending) {
|
||||
await getRepository(User).update(
|
||||
{ id: user.id },
|
||||
{ status: StatusType.Active }
|
||||
const updated = await AppDataSource.transaction(
|
||||
async (entityManager) => {
|
||||
await setClaims(entityManager, user.id)
|
||||
return entityManager
|
||||
.getRepository(User)
|
||||
.update({ id: user.id }, { status: StatusType.Active })
|
||||
}
|
||||
)
|
||||
|
||||
if (!updated.affected) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/auth/confirm-email?errorCodes=UNKNOWN`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
res.set('Message', 'EMAIL_CONFIRMED')
|
||||
await setAuthInCookie({ uid: user.id }, res)
|
||||
await handleSuccessfulLogin(req, res, user, false)
|
||||
} catch (e) {
|
||||
logger.info('confirm-email exception:', e)
|
||||
if (e instanceof jwt.TokenExpiredError) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/confirm-email?errorCodes=TOKEN_EXPIRED`
|
||||
`${env.client.url}/auth/confirm-email?errorCodes=TOKEN_EXPIRED`
|
||||
)
|
||||
}
|
||||
|
||||
res.redirect(`${env.client.url}/confirm-email?errorCodes=INVALID_TOKEN`)
|
||||
res.redirect(
|
||||
`${env.client.url}/auth/confirm-email?errorCodes=INVALID_TOKEN`
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -537,7 +550,7 @@ export function authRouter() {
|
|||
const email = req.body.email
|
||||
if (!email) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/forgot-password?errorCodes=INVALID_EMAIL`
|
||||
`${env.client.url}/auth/forgot-password?errorCodes=INVALID_EMAIL`
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -546,28 +559,26 @@ export function authRouter() {
|
|||
email,
|
||||
})
|
||||
if (!user) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/forgot-password?errorCodes=USER_NOT_FOUND`
|
||||
)
|
||||
return res.redirect(`${env.client.url}/auth/reset-sent`)
|
||||
}
|
||||
|
||||
if (user.status === StatusType.Pending) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/email-login?errorCodes=PENDING_VERIFICATION`
|
||||
)
|
||||
return res.redirect(`${env.client.url}/auth/reset-sent`)
|
||||
}
|
||||
|
||||
if (!(await sendPasswordResetEmail(user))) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/forgot-password?errorCodes=INVALID_EMAIL`
|
||||
`${env.client.url}/auth/forgot-password?errorCodes=INVALID_EMAIL`
|
||||
)
|
||||
}
|
||||
|
||||
res.redirect(`${env.client.url}/forgot-password?message=SUCCESS`)
|
||||
res.redirect(`${env.client.url}/auth/reset-sent`)
|
||||
} catch (e) {
|
||||
logger.info('forgot-password exception:', e)
|
||||
|
||||
res.redirect(`${env.client.url}/forgot-password?errorCodes=UNKNOWN`)
|
||||
res.redirect(
|
||||
`${env.client.url}/auth/forgot-password?errorCodes=UNKNOWN`
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -588,51 +599,55 @@ export function authRouter() {
|
|||
const claims = await getClaimsByToken(token)
|
||||
if (!claims) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/reset-password?errorCodes=INVALID_TOKEN`
|
||||
`${env.client.url}/auth/reset-password/${token}?errorCodes=INVALID_TOKEN`
|
||||
)
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
if (!password || password.length < 8) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/reset-password?errorCodes=INVALID_PASSWORD`
|
||||
`${env.client.url}/auth/reset-password/${token}?errorCodes=INVALID_PASSWORD`
|
||||
)
|
||||
}
|
||||
|
||||
const user = await getRepository(User).findOneBy({ id: claims.uid })
|
||||
if (!user) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/reset-password?errorCodes=USER_NOT_FOUND`
|
||||
`${env.client.url}/auth/reset-password/${token}?errorCodes=USER_NOT_FOUND`
|
||||
)
|
||||
}
|
||||
|
||||
if (user.status === StatusType.Pending) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/email-login?errorCodes=PENDING_VERIFICATION`
|
||||
`${env.client.url}/auth/email-login?errorCodes=PENDING_VERIFICATION`
|
||||
)
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password)
|
||||
const updated = await getRepository(User).update(
|
||||
{ id: user.id },
|
||||
{ password: hashedPassword }
|
||||
const updated = await AppDataSource.transaction(
|
||||
async (entityManager) => {
|
||||
await setClaims(entityManager, user.id)
|
||||
return entityManager
|
||||
.getRepository(User)
|
||||
.update({ id: user.id }, { password: hashedPassword })
|
||||
}
|
||||
)
|
||||
if (!updated.affected) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/reset-password?errorCodes=UNKNOWN`
|
||||
`${env.client.url}/auth/reset-password/${token}?errorCodes=UNKNOWN`
|
||||
)
|
||||
}
|
||||
|
||||
res.redirect(`${env.client.url}/reset-password?message=SUCCESS`)
|
||||
await handleSuccessfulLogin(req, res, user, false)
|
||||
} catch (e) {
|
||||
logger.info('reset-password exception:', e)
|
||||
if (e instanceof jwt.TokenExpiredError) {
|
||||
return res.redirect(
|
||||
`${env.client.url}/reset-password?errorCodes=TOKEN_EXPIRED`
|
||||
`${env.client.url}/auth/reset-password/?errorCodes=TOKEN_EXPIRED`
|
||||
)
|
||||
}
|
||||
|
||||
res.redirect(
|
||||
`${env.client.url}/reset-password?errorCodes=INVALID_TOKEN`
|
||||
`${env.client.url}/auth/reset-password/?errorCodes=INVALID_TOKEN`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const sendConfirmationEmail = async (user: {
|
|||
}): Promise<boolean> => {
|
||||
// generate confirmation link
|
||||
const token = generateVerificationToken(user.id)
|
||||
const link = `${env.client.url}/confirm-email/${token}`
|
||||
const link = `${env.client.url}/auth/confirm-email/${token}`
|
||||
// send email
|
||||
const dynamicTemplateData = {
|
||||
name: user.name,
|
||||
|
|
@ -31,7 +31,7 @@ export const sendPasswordResetEmail = async (user: {
|
|||
}): Promise<boolean> => {
|
||||
// generate link
|
||||
const token = generateVerificationToken(user.id)
|
||||
const link = `${env.client.url}/reset-password/${token}`
|
||||
const link = `${env.client.url}/auth/reset-password/${token}`
|
||||
// send email
|
||||
const dynamicTemplateData = {
|
||||
name: user.name,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ describe('auth router', () => {
|
|||
sinon.restore()
|
||||
})
|
||||
|
||||
it('redirects to login page', async () => {
|
||||
it('redirects to verify email', async () => {
|
||||
const res = await signupRequest(
|
||||
email,
|
||||
password,
|
||||
|
|
@ -69,7 +69,7 @@ describe('auth router', () => {
|
|||
username
|
||||
).expect(302)
|
||||
expect(res.header.location).to.endWith(
|
||||
'/email-login?message=SIGNUP_SUCCESS'
|
||||
'/verify-email?message=SIGNUP_SUCCESS'
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -173,9 +173,9 @@ describe('auth router', () => {
|
|||
password = correctPassword
|
||||
})
|
||||
|
||||
it('redirects to home page', async () => {
|
||||
it('redirects to sso page', async () => {
|
||||
const res = await loginRequest(email, password).expect(302)
|
||||
expect(res.header.location).to.endWith('/home')
|
||||
expect(res.header.location).to.contain('/api/client/auth?tok')
|
||||
})
|
||||
|
||||
it('set auth token in cookie', async () => {
|
||||
|
|
@ -297,9 +297,9 @@ describe('auth router', () => {
|
|||
expect(res.header['set-cookie'][0]).to.contain('auth')
|
||||
})
|
||||
|
||||
it('redirects to home page', async () => {
|
||||
it('redirects to sso page', async () => {
|
||||
const res = await confirmEmailRequest(token).expect(302)
|
||||
expect(res.header.location).to.endWith('/home?message=EMAIL_CONFIRMED')
|
||||
expect(res.header.location).to.contain('/api/client/auth?tok')
|
||||
})
|
||||
|
||||
it('sets user as active', async () => {
|
||||
|
|
@ -395,7 +395,7 @@ describe('auth router', () => {
|
|||
it('redirects to forgot-password page with success message', async () => {
|
||||
const res = await emailResetPasswordReq(email).expect(302)
|
||||
expect(res.header.location).to.endWith(
|
||||
'/forgot-password?message=SUCCESS'
|
||||
'/auth/reset-sent'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -432,7 +432,7 @@ describe('auth router', () => {
|
|||
it('redirects to email-login page with error code PENDING_VERIFICATION', async () => {
|
||||
const res = await emailResetPasswordReq(email).expect(302)
|
||||
expect(res.header.location).to.endWith(
|
||||
'/email-login?errorCodes=PENDING_VERIFICATION'
|
||||
'/auth/reset-sent'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -446,7 +446,7 @@ describe('auth router', () => {
|
|||
it('redirects to forgot-password page with error code USER_NOT_FOUND', async () => {
|
||||
const res = await emailResetPasswordReq(email).expect(302)
|
||||
expect(res.header.location).to.endWith(
|
||||
'/forgot-password?errorCodes=USER_NOT_FOUND'
|
||||
'/auth/reset-sent'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -498,8 +498,8 @@ describe('auth router', () => {
|
|||
const res = await resetPasswordRequest(token, 'new_password').expect(
|
||||
302
|
||||
)
|
||||
expect(res.header.location).to.endWith(
|
||||
'/reset-password?message=SUCCESS'
|
||||
expect(res.header.location).to.contain(
|
||||
'/api/client/auth?tok'
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -517,8 +517,8 @@ describe('auth router', () => {
|
|||
context('when password is empty', () => {
|
||||
it('redirects to reset-password page with error code INVALID_PASSWORD', async () => {
|
||||
const res = await resetPasswordRequest(token, '').expect(302)
|
||||
expect(res.header.location).to.endWith(
|
||||
'/reset-password?errorCodes=INVALID_PASSWORD'
|
||||
expect(res.header.location).to.match(
|
||||
/.*\/auth\/reset-password\/(.*)?\?errorCodes=INVALID_PASSWORD/g
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -530,8 +530,8 @@ describe('auth router', () => {
|
|||
'invalid_token',
|
||||
'new_password'
|
||||
).expect(302)
|
||||
expect(res.header.location).to.endWith(
|
||||
'/reset-password?errorCodes=INVALID_TOKEN'
|
||||
expect(res.header.location).to.match(
|
||||
/.*\/auth\/reset-password\/(.*)?\?errorCodes=INVALID_TOKEN/g
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -545,7 +545,7 @@ describe('auth router', () => {
|
|||
302
|
||||
)
|
||||
expect(res.header.location).to.endWith(
|
||||
'/reset-password?errorCodes=TOKEN_EXPIRED'
|
||||
'/auth/reset-password/?errorCodes=TOKEN_EXPIRED'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -106,33 +106,6 @@ export function LoginForm(props: LoginFormProps): JSX.Element {
|
|||
/>
|
||||
</Box>
|
||||
)}
|
||||
{/*
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{
|
||||
pt: '16px',
|
||||
maxWidth: '220px',
|
||||
textAlign: 'left',
|
||||
color: '$omnivoreLightGray',
|
||||
}}
|
||||
>
|
||||
<SpanBox>
|
||||
Use your email address to{' '}
|
||||
<Link href="/email-login" passHref>
|
||||
<StyledTextSpan style="captionLink" css={{ color: '$omnivoreGray' }}>
|
||||
Login
|
||||
</StyledTextSpan>
|
||||
</Link>{' '}
|
||||
or{' '}
|
||||
<Link href="/email-signup" passHref>
|
||||
<StyledTextSpan style="captionLink" css={{ color: '$omnivoreGray' }}>
|
||||
Signup
|
||||
</StyledTextSpan>
|
||||
</Link>{' '}
|
||||
with your email address.
|
||||
</SpanBox>
|
||||
</StyledText> */}
|
||||
|
||||
</VStack>
|
||||
<TermAndConditionsFooter />
|
||||
</VStack>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,21 @@
|
|||
import { SpanBox, VStack } from '../elements/LayoutPrimitives'
|
||||
import { Button } from '../elements/Button'
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import { SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { FormInput } from '../elements/FormElements'
|
||||
import { TermAndConditionsFooter } from './LoginForm'
|
||||
import { fetchEndpoint } from '../../lib/appConfig'
|
||||
import { logoutMutation } from '../../lib/networking/mutations/logoutMutation'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
import { fetchEndpoint } from '../../../lib/appConfig'
|
||||
import { logoutMutation } from '../../../lib/networking/mutations/logoutMutation'
|
||||
import { styled } from '@stitches/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { formatMessage } from '../../locales/en/messages'
|
||||
import { parseErrorCodes } from '../../lib/queryParamParser'
|
||||
|
||||
const StyledTextSpan = styled('span', StyledText)
|
||||
import { formatMessage } from '../../../locales/en/messages'
|
||||
import { parseErrorCodes } from '../../../lib/queryParamParser'
|
||||
|
||||
const BorderedFormInput = styled(FormInput, {
|
||||
height: '40px',
|
||||
paddingLeft: '6px',
|
||||
borderRadius: '6px',
|
||||
background: 'white',
|
||||
color: '$omnivoreGray',
|
||||
border: `1px solid 1px solid rgba(0, 0, 0, 0.06)`,
|
||||
})
|
||||
|
||||
|
|
@ -26,9 +24,9 @@ const FormLabel = styled('label', {
|
|||
color: '$omnivoreGray',
|
||||
})
|
||||
|
||||
export function EmailResetPassword(): JSX.Element {
|
||||
export function EmailForgotPassword(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const [email, setEmail] = useState<string | undefined>(undefined)
|
||||
const [email, setEmail] = useState<string>('')
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -41,7 +39,7 @@ export function EmailResetPassword(): JSX.Element {
|
|||
}, [router.isReady, router.query])
|
||||
|
||||
return (
|
||||
<form action={`${fetchEndpoint}/auth/email-signup`} method="POST">
|
||||
<form action={`${fetchEndpoint}/auth/forgot-password`} method="POST">
|
||||
<VStack alignment="center" css={{ padding: '16px' }}>
|
||||
<StyledText style="subHeadline">Reset your password</StyledText>
|
||||
<VStack css={{ width: '100%', minWidth: '320px', gap: '16px', pb: '16px' }}>
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { HStack, SpanBox, VStack } from '../elements/LayoutPrimitives'
|
||||
import { Button } from '../elements/Button'
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { FormInput } from '../elements/FormElements'
|
||||
import { TermAndConditionsFooter } from './LoginForm'
|
||||
import { fetchEndpoint } from '../../lib/appConfig'
|
||||
import { logoutMutation } from '../../lib/networking/mutations/logoutMutation'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
import { TermAndConditionsFooter } from '../LoginForm'
|
||||
import { fetchEndpoint } from '../../../lib/appConfig'
|
||||
import { logoutMutation } from '../../../lib/networking/mutations/logoutMutation'
|
||||
import { styled } from '@stitches/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { parseErrorCodes } from '../../lib/queryParamParser'
|
||||
import { formatMessage } from '../../locales/en/messages'
|
||||
import { parseErrorCodes } from '../../../lib/queryParamParser'
|
||||
import { formatMessage } from '../../../locales/en/messages'
|
||||
import Link from 'next/link'
|
||||
|
||||
const StyledTextSpan = styled('span', StyledText)
|
||||
|
|
@ -45,7 +45,7 @@ export function EmailLogin(): JSX.Element {
|
|||
return (
|
||||
<form action={`${fetchEndpoint}/auth/email-login`} method="POST">
|
||||
<VStack alignment="center" css={{ padding: '16px' }}>
|
||||
<StyledText style="subHeadline">Login</StyledText>
|
||||
<StyledText style="subHeadline" css={{ color: '$omnivoreGray' }}>Login</StyledText>
|
||||
<VStack css={{ width: '100%', minWidth: '320px', gap: '16px', pb: '16px' }}>
|
||||
<SpanBox css={{ width: '100%' }}>
|
||||
<FormLabel>Email</FormLabel>
|
||||
|
|
@ -111,7 +111,7 @@ export function EmailLogin(): JSX.Element {
|
|||
}}
|
||||
>
|
||||
Don't have an account? {' '}
|
||||
<Link href="/email-signup" passHref>
|
||||
<Link href="/auth/email-signup" passHref>
|
||||
<StyledTextSpan style="captionLink" css={{ color: '$omnivoreGray' }}>Sign up</StyledTextSpan>
|
||||
</Link>
|
||||
</StyledText>
|
||||
|
|
@ -126,7 +126,7 @@ export function EmailLogin(): JSX.Element {
|
|||
}}
|
||||
>
|
||||
Forgot your password? {' '}
|
||||
<Link href="/email-reset-password" passHref>
|
||||
<Link href="/auth/forgot-password" passHref>
|
||||
<StyledTextSpan style="captionLink" css={{ color: '$omnivoreGray' }}>Click here</StyledTextSpan>
|
||||
</Link>
|
||||
</StyledText>
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import { SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
import { TermAndConditionsFooter } from '../LoginForm'
|
||||
import { fetchEndpoint } from '../../../lib/appConfig'
|
||||
import { logoutMutation } from '../../../lib/networking/mutations/logoutMutation'
|
||||
import { styled } from '@stitches/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { formatMessage } from '../../../locales/en/messages'
|
||||
import { parseErrorCodes } from '../../../lib/queryParamParser'
|
||||
import { LoadingView } from '../../patterns/LoadingView'
|
||||
|
||||
const BorderedFormInput = styled(FormInput, {
|
||||
height: '40px',
|
||||
paddingLeft: '6px',
|
||||
borderRadius: '6px',
|
||||
background: 'white',
|
||||
color: '$omnivoreGray',
|
||||
border: `1px solid 1px solid rgba(0, 0, 0, 0.06)`,
|
||||
})
|
||||
|
||||
const FormLabel = styled('label', {
|
||||
fontSize: '16px',
|
||||
color: '$omnivoreGray',
|
||||
})
|
||||
|
||||
export function EmailResetPassword(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const [token, setToken] = useState<string | undefined>(undefined)
|
||||
const [password, setPassword] = useState<string>('')
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
const errorCode = parseErrorCodes(router.query)
|
||||
const errorMsg = errorCode
|
||||
? formatMessage({ id: `error.${errorCode}` })
|
||||
: undefined
|
||||
|
||||
console.log('errorCode', errorCode, errorMsg)
|
||||
|
||||
setErrorMessage(errorMsg)
|
||||
setToken(router.query.token as string)
|
||||
}, [router.isReady, router.query])
|
||||
|
||||
if (!token) {
|
||||
return <LoadingView />
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={`${fetchEndpoint}/auth/reset-password`} method="POST">
|
||||
<VStack alignment="center" css={{ padding: '16px' }}>
|
||||
<VStack css={{ width: '100%', minWidth: '320px', gap: '16px', pb: '16px' }}>
|
||||
<SpanBox css={{ width: '100%' }}>
|
||||
<FormLabel>Enter new password</FormLabel>
|
||||
<BorderedFormInput
|
||||
type="password"
|
||||
key="password"
|
||||
name="password"
|
||||
value={password}
|
||||
placeholder="Password"
|
||||
onChange={(e) => { e.preventDefault(); setPassword(e.target.value); }}
|
||||
/>
|
||||
<FormLabel css={{ fontSize: '12px' }}>(Password must be at least 8 chars)</FormLabel>
|
||||
|
||||
<input type="hidden" name="token" value={token} />
|
||||
</SpanBox>
|
||||
</VStack>
|
||||
|
||||
{errorMessage && (
|
||||
<StyledText style="error">{errorMessage}</StyledText>
|
||||
)}
|
||||
<Button type="submit" style="ctaDarkYellow" css={{ my: '$2' }}>
|
||||
Update Password
|
||||
</Button>
|
||||
</VStack>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
import { HStack, SpanBox, VStack } from '../elements/LayoutPrimitives'
|
||||
import { Button } from '../elements/Button'
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { FormInput } from '../elements/FormElements'
|
||||
import { TermAndConditionsFooter } from './LoginForm'
|
||||
import { fetchEndpoint } from '../../lib/appConfig'
|
||||
import { useValidateUsernameQuery } from '../../lib/networking/queries/useValidateUsernameQuery'
|
||||
import { logoutMutation } from '../../lib/networking/mutations/logoutMutation'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
import { TermAndConditionsFooter } from '../LoginForm'
|
||||
import { fetchEndpoint } from '../../../lib/appConfig'
|
||||
import { useValidateUsernameQuery } from '../../../lib/networking/queries/useValidateUsernameQuery'
|
||||
import { logoutMutation } from '../../../lib/networking/mutations/logoutMutation'
|
||||
import { styled } from '@stitches/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { formatMessage } from '../../locales/en/messages'
|
||||
import { parseErrorCodes } from '../../lib/queryParamParser'
|
||||
import { formatMessage } from '../../../locales/en/messages'
|
||||
import { parseErrorCodes } from '../../../lib/queryParamParser'
|
||||
import Link from 'next/link'
|
||||
|
||||
const StyledTextSpan = styled('span', StyledText)
|
||||
|
|
@ -173,7 +173,7 @@ export function EmailSignup(): JSX.Element {
|
|||
}}
|
||||
>
|
||||
Already have an account? {' '}
|
||||
<Link href="/email-login" passHref>
|
||||
<Link href="/auth/email-login" passHref>
|
||||
<StyledTextSpan style="captionLink" css={{ color: '$omnivoreGray' }}>Login instead</StyledTextSpan>
|
||||
</Link>
|
||||
</StyledText>
|
||||
33
packages/web/components/templates/auth/ResetSent.tsx
Normal file
33
packages/web/components/templates/auth/ResetSent.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { Box, HStack } from '../../elements/LayoutPrimitives'
|
||||
import type { LoginFormProps } from '../LoginForm'
|
||||
|
||||
export function ResetSent(props: LoginFormProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
bg: '$omnivoreYellow',
|
||||
overflowY: 'clip'
|
||||
}}
|
||||
>
|
||||
<Box css={{
|
||||
width: '100%',
|
||||
margin: '40px',
|
||||
color: '$omnivoreGray',
|
||||
'@xl': { margin: '138px' },
|
||||
}}>
|
||||
<h1>Reset email sent</h1>
|
||||
<Box>
|
||||
If there is an account assosciated with the email specified we sent a
|
||||
password reset link. Click the link to reset your password. You may need
|
||||
to check your spam folder.
|
||||
</Box>
|
||||
</Box>
|
||||
</HStack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
35
packages/web/components/templates/auth/VerifyEmail.tsx
Normal file
35
packages/web/components/templates/auth/VerifyEmail.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { Box, HStack, MediumBreakpointBox, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import type { LoginFormProps } from '../LoginForm'
|
||||
import { OmnivoreNameLogo } from '../../elements/images/OmnivoreNameLogo'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
|
||||
export function VerifyEmail(props: LoginFormProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
bg: '$omnivoreYellow',
|
||||
overflowY: 'clip'
|
||||
}}
|
||||
>
|
||||
<Box css={{
|
||||
width: '100%',
|
||||
margin: '40px',
|
||||
color: '$omnivoreGray',
|
||||
'@xl': { margin: '138px' },
|
||||
}}>
|
||||
<h1>Verify your email address</h1>
|
||||
<Box>
|
||||
We sent a verification link to the email you provided.
|
||||
Click the link to verify your email. You may need to check
|
||||
your spam folder.
|
||||
</Box>
|
||||
</Box>
|
||||
</HStack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -24,6 +24,8 @@ const errorMessages: Record<string, string> = {
|
|||
'error.EXPIRED_TOKEN':
|
||||
"Your sign up page has timed out, you'll be redirected to Google sign in page to authenticate again.",
|
||||
'error.USER_EXISTS': 'User with this email exists already',
|
||||
'error.UNKNOWN': 'An unknown error occurred',
|
||||
'error.INVALID_PASSWORD': 'Invalid password. Password must be at least 8 chars.'
|
||||
}
|
||||
|
||||
const loginPageMessages: Record<string, string> = {
|
||||
|
|
|
|||
40
packages/web/pages/auth/confirm-email/[token].tsx
Normal file
40
packages/web/pages/auth/confirm-email/[token].tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { HStack } from '../../../components/elements/LayoutPrimitives'
|
||||
import { fetchEndpoint } from '../../../lib/appConfig'
|
||||
import { LoadingView } from '../../../components/patterns/LoadingView'
|
||||
import { PageMetaData } from '../../../components/patterns/PageMetaData'
|
||||
import { ProfileLayout } from '../../../components/templates/ProfileLayout'
|
||||
|
||||
export default function ConfirmEmail(): JSX.Element {
|
||||
const authForm = useRef<HTMLFormElement | null>(null)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (!router || !router.isReady || !authForm.current) {
|
||||
return
|
||||
}
|
||||
|
||||
authForm.current?.submit()
|
||||
}, [router, authForm])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMetaData title="Confirm Email - Omnivore" path="/confirm-email" />
|
||||
<ProfileLayout>
|
||||
<form
|
||||
ref={authForm}
|
||||
method="POST"
|
||||
action={`${fetchEndpoint}/auth/confirm-email`}
|
||||
>
|
||||
<input type="hidden" name="token" value={router.query.token} />
|
||||
</form>
|
||||
<HStack css={{ bg: '$grayBg', padding: '24px', width: '100%', height: '100%'}}>
|
||||
<LoadingView />
|
||||
</HStack>
|
||||
</ProfileLayout>
|
||||
<div data-testid="confirm-email-page-tag" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
15
packages/web/pages/auth/email-login.tsx
Normal file
15
packages/web/pages/auth/email-login.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { PageMetaData } from '../../components/patterns/PageMetaData'
|
||||
import { ProfileLayout } from '../../components/templates/ProfileLayout'
|
||||
import { EmailLogin } from '../../components/templates/auth/EmailLogin'
|
||||
|
||||
export default function EmailLoginPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<PageMetaData title="Login - Omnivore" path="/email-login" />
|
||||
<ProfileLayout>
|
||||
<EmailLogin />
|
||||
</ProfileLayout>
|
||||
<div data-testid="email-login-page-tag" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
15
packages/web/pages/auth/email-signup.tsx
Normal file
15
packages/web/pages/auth/email-signup.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { PageMetaData } from '../../components/patterns/PageMetaData'
|
||||
import { ProfileLayout } from '../../components/templates/ProfileLayout'
|
||||
import { EmailSignup } from '../../components/templates/auth/EmailSignup'
|
||||
|
||||
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" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
24
packages/web/pages/auth/forgot-password.tsx
Normal file
24
packages/web/pages/auth/forgot-password.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { PageMetaData } from '../../components/patterns/PageMetaData'
|
||||
import { ProfileLayout } from '../../components/templates/ProfileLayout'
|
||||
import { EmailForgotPassword } from '../../components/templates/auth/EmailForgotPassword'
|
||||
import { useEffect } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import toast, { Toaster } from 'react-hot-toast'
|
||||
import { showSuccessToast } from '../../lib/toastHelpers'
|
||||
|
||||
export default function ForgotPassword(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<PageMetaData title="Reset your password - Omnivore" path="/auth-forgot-password" />
|
||||
<Toaster
|
||||
containerStyle={{
|
||||
top: '5rem',
|
||||
}}
|
||||
/>
|
||||
<ProfileLayout>
|
||||
<EmailForgotPassword />
|
||||
</ProfileLayout>
|
||||
<div data-testid="auth-forgot-password-page-tag" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
15
packages/web/pages/auth/reset-password/[token].tsx
Normal file
15
packages/web/pages/auth/reset-password/[token].tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { PageMetaData } from '../../../components/patterns/PageMetaData'
|
||||
import { ProfileLayout } from '../../../components/templates/ProfileLayout'
|
||||
import { EmailResetPassword } from '../../../components/templates/auth/EmailResetPassword'
|
||||
|
||||
export default function EmailRegistrationPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<PageMetaData title="Reset your password - Omnivore" path="/auth-forgot-password" />
|
||||
<ProfileLayout>
|
||||
<EmailResetPassword />
|
||||
</ProfileLayout>
|
||||
<div data-testid="auth-forgot-password-page-tag" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
15
packages/web/pages/auth/reset-sent.tsx
Normal file
15
packages/web/pages/auth/reset-sent.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { PageMetaData } from '../../components/patterns/PageMetaData'
|
||||
import { ProfileLayout } from '../../components/templates/ProfileLayout'
|
||||
import { ResetSent } from '../../components/templates/auth/ResetSent'
|
||||
|
||||
export default function EmailResetSent(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<PageMetaData title="Reset password email sent - Omnivore" path="/auth-reset-sent" />
|
||||
<ProfileLayout>
|
||||
<ResetSent />
|
||||
</ProfileLayout>
|
||||
<div data-testid="auth-reset-sent-page-tag" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
12
packages/web/pages/auth/verify-email.tsx
Normal file
12
packages/web/pages/auth/verify-email.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { PageMetaData } from '../../components/patterns/PageMetaData'
|
||||
import { VerifyEmail } from '../../components/templates/auth/VerifyEmail'
|
||||
|
||||
export default function VerifyEmailPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<PageMetaData title="Verify Email" path="/auth/verify-email" />
|
||||
<VerifyEmail />
|
||||
<div data-testid="auth/verify-email-page-tag" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
import { PageMetaData } from '../components/patterns/PageMetaData'
|
||||
import { ProfileLayout } from '../components/templates/ProfileLayout'
|
||||
import { EmailLogin } from '../components/templates/EmailLogin'
|
||||
|
||||
export default function EmailLoginPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<PageMetaData title="Login - Omnivore" path="/email-login" />
|
||||
<ProfileLayout>
|
||||
<EmailLogin />
|
||||
</ProfileLayout>
|
||||
<div data-testid="email-login-page-tag" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// export default function EmailLogin(): JSX.Element {
|
||||
// const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
// undefined
|
||||
// )
|
||||
// const [message, setMessage] = useState<string | undefined>(undefined)
|
||||
// const router = useRouter()
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!router.isReady) return
|
||||
// const errorCode = parseErrorCodes(router.query)
|
||||
// const errorMsg = errorCode
|
||||
// ? formatMessage({ id: `error.${errorCode}` })
|
||||
// : undefined
|
||||
// setErrorMessage(errorMsg)
|
||||
|
||||
// const message = router.query.message
|
||||
// ? formatMessage({ id: `login.${router.query.message}` })
|
||||
// : undefined
|
||||
// setMessage(message)
|
||||
// }, [router.isReady, router.query])
|
||||
|
||||
// return (
|
||||
// <PrimaryLayout pageTestId="email-login">
|
||||
// {message && <StyledText style={'headline'}>{message}</StyledText>}
|
||||
// <h1>Email Login</h1>
|
||||
// <form action={`${fetchEndpoint}/auth/email-login`} method="POST">
|
||||
// <div>
|
||||
// <label>Email</label>
|
||||
// <input type="email" name={'email'} required />
|
||||
// </div>
|
||||
// <div>
|
||||
// <label>Password</label>
|
||||
// <input type="password" name={'password'} required />
|
||||
// </div>
|
||||
// {errorMessage && <StyledText style="error">{errorMessage}</StyledText>}
|
||||
// <button type="submit">Login</button>
|
||||
// </form>
|
||||
// </PrimaryLayout>
|
||||
// )
|
||||
// }
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { PageMetaData } from '../components/patterns/PageMetaData'
|
||||
import { ProfileLayout } from '../components/templates/ProfileLayout'
|
||||
import { EmailResetPassword } from '../components/templates/EmailResetPassword'
|
||||
|
||||
export default function EmailRegistrationPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<PageMetaData title="Reset your password - Omnivore" path="/email-reset-password" />
|
||||
<ProfileLayout>
|
||||
<EmailResetPassword />
|
||||
</ProfileLayout>
|
||||
<div data-testid="email-reset-password-page-tag" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { PageMetaData } from '../components/patterns/PageMetaData'
|
||||
import { ProfileLayout } from '../components/templates/ProfileLayout'
|
||||
import { EmailSignup } from '../components/templates/EmailSignup'
|
||||
|
||||
export default function EmailRegistrationPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<PageMetaData title="Sign up with Email - Omnivore" path="/email-signup" />
|
||||
<ProfileLayout>
|
||||
<EmailSignup />
|
||||
</ProfileLayout>
|
||||
<div data-testid="email-signup-page-tag" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue