Handle resetting passwords

This commit is contained in:
Jackson Harper
2022-07-26 12:33:56 -07:00
parent acd36fe9af
commit dc268a2216
10 changed files with 194 additions and 96 deletions

View File

@ -557,13 +557,13 @@ export function authRouter() {
})
if (!user) {
return res.redirect(
`${env.client.url}/auth/forgot-password?errorCodes=USER_NOT_FOUND`
`${env.client.url}/auth/auth/reset-sent`
)
}
if (user.status === StatusType.Pending) {
return res.redirect(
`${env.client.url}/auth/email-login?errorCodes=PENDING_VERIFICATION`
`${env.client.url}/auth/auth/reset-sent`
)
}
@ -573,7 +573,7 @@ export function authRouter() {
)
}
res.redirect(`${env.client.url}/auth/forgot-password?message=SUCCESS`)
res.redirect(`${env.client.url}/auth/reset-sent`)
} catch (e) {
logger.info('forgot-password exception:', e)
@ -598,20 +598,20 @@ export function authRouter() {
const claims = await getClaimsByToken(token)
if (!claims) {
return res.redirect(
`${env.client.url}/auth/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}/auth/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}/auth/reset-password?errorCodes=USER_NOT_FOUND`
`${env.client.url}/auth/reset-password/${token}?errorCodes=USER_NOT_FOUND`
)
}
@ -632,11 +632,11 @@ export function authRouter() {
)
if (!updated.affected) {
return res.redirect(
`${env.client.url}/auth/reset-password?errorCodes=UNKNOWN`
`${env.client.url}/auth/reset-password/${token}?errorCodes=UNKNOWN`
)
}
res.redirect(`${env.client.url}/auth/reset-password?message=SUCCESS`)
await handleSuccessfulLogin(req, res, user, false)
} catch (e) {
logger.info('reset-password exception:', e)
if (e instanceof jwt.TokenExpiredError) {

View File

@ -0,0 +1,92 @@
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 { 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 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 EmailForgotPassword(): JSX.Element {
const router = useRouter()
const [email, setEmail] = 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
setErrorMessage(errorMsg)
}, [router.isReady, router.query])
return (
<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' }}>
<SpanBox css={{ width: '100%' }}>
<FormLabel>Email</FormLabel>
<BorderedFormInput
key="email"
type="text"
name="email"
value={email}
placeholder="Email"
onChange={(e) => { e.preventDefault(); setEmail(e.target.value); }}
/>
</SpanBox>
</VStack>
{errorMessage && (
<StyledText style="error">{errorMessage}</StyledText>
)}
<Button type="submit" style="ctaDarkYellow" css={{ my: '$2' }}>
Reset Password
</Button>
<Button
style="ghost"
onClick={async (e) => {
e.preventDefault()
window.localStorage.removeItem('authVerified')
window.localStorage.removeItem('authToken')
try {
await logoutMutation()
} catch (e) {
console.log('error logging out', e)
}
window.location.href = '/'
}}
>
<StyledText
css={{
color: '$omnivoreRed',
textDecoration: 'underline',
cursor: 'pointer',
}}
>
Cancel
</StyledText>
</Button>
</VStack>
</form>
)
}

View File

@ -10,14 +10,14 @@ 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 { 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)`,
})
@ -28,7 +28,8 @@ const FormLabel = styled('label', {
export function EmailResetPassword(): JSX.Element {
const router = useRouter()
const [email, setEmail] = useState<string | undefined>(undefined)
const [token, setToken] = useState<string | undefined>(undefined)
const [password, setPassword] = useState<string>('')
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
useEffect(() => {
@ -37,24 +38,34 @@ export function EmailResetPassword(): JSX.Element {
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/email-signup`} method="POST">
<form action={`${fetchEndpoint}/auth/reset-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' }}>
<SpanBox css={{ width: '100%' }}>
<FormLabel>Email</FormLabel>
<FormLabel>Enter new password</FormLabel>
<BorderedFormInput
key="email"
type="text"
name="email"
value={email}
placeholder="Email"
onChange={(e) => { e.preventDefault(); setEmail(e.target.value); }}
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>
@ -62,31 +73,7 @@ export function EmailResetPassword(): JSX.Element {
<StyledText style="error">{errorMessage}</StyledText>
)}
<Button type="submit" style="ctaDarkYellow" css={{ my: '$2' }}>
Reset Password
</Button>
<Button
style="ghost"
onClick={async (e) => {
e.preventDefault()
window.localStorage.removeItem('authVerified')
window.localStorage.removeItem('authToken')
try {
await logoutMutation()
} catch (e) {
console.log('error logging out', e)
}
window.location.href = '/'
}}
>
<StyledText
css={{
color: '$omnivoreRed',
textDecoration: 'underline',
cursor: 'pointer',
}}
>
Cancel
</StyledText>
Update Password
</Button>
</VStack>
</form>

View 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>
</>
)
}

View File

@ -24,30 +24,12 @@ export function VerifyEmail(props: LoginFormProps): JSX.Element {
}}>
<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.
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>
<Box
css={{
position: 'absolute',
top: 0,
left: 0,
p: '0px 15px 0px 15px',
height: '68px',
minHeight: '68px',
display: 'flex',
alignItems: 'center',
'@md': { width: '50%' },
'@xsDown': { height: '48px' },
justifyContent: 'space-between',
width: '100%',
}}
>
<OmnivoreNameLogo color={theme.colors.omnivoreGray.toString()} href='/login' />
</Box>
</>
)
}

View File

@ -25,6 +25,7 @@ const errorMessages: Record<string, string> = {
"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> = {

View File

@ -1,24 +1,12 @@
import { PageMetaData } from '../../components/patterns/PageMetaData'
import { ProfileLayout } from '../../components/templates/ProfileLayout'
import { EmailResetPassword } from '../../components/templates/EmailResetPassword'
import { EmailForgotPassword } from '../../components/templates/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 {
const router = useRouter()
useEffect(() => {
if (router && router.isReady && router.query.message === 'SUCCESS') {
showSuccessToast('Reset password email sent')
setTimeout(() => {
window.location.href = '/email-login'
}, 2000)
}
}, [router])
return (
<>
<PageMetaData title="Reset your password - Omnivore" path="/auth-forgot-password" />
@ -28,7 +16,7 @@ export default function ForgotPassword(): JSX.Element {
}}
/>
<ProfileLayout>
<EmailResetPassword />
<EmailForgotPassword />
</ProfileLayout>
<div data-testid="auth-forgot-password-page-tag" />
</>

View File

@ -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="Forgot your password - Omnivore" path="/auth-forgot-password" />
<ProfileLayout>
<EmailResetPassword />
</ProfileLayout>
<div data-testid="auth-forgot-password-page-tag" />
</>
)
}

View File

@ -0,0 +1,15 @@
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="/auth-forgot-password" />
<ProfileLayout>
<EmailResetPassword />
</ProfileLayout>
<div data-testid="auth-forgot-password-page-tag" />
</>
)
}

View File

@ -0,0 +1,15 @@
import { PageMetaData } from '../../components/patterns/PageMetaData'
import { ProfileLayout } from '../../components/templates/ProfileLayout'
import { ResetSent } from '../../components/templates/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" />
</>
)
}