Skip to content

Commit

Permalink
fix: handle expired/invalid cognito code (#1275)
Browse files Browse the repository at this point in the history
<!--- Please provide a general summary of your changes in the title
above -->

# Pull Request type

<!-- Please try to limit your pull request to one type; submit multiple
pull requests if needed. -->

Please check the type of change your PR introduces:

- [x] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no API changes)
- [ ] Build-related changes
- [ ] Documentation content changes
- [ ] Other (please describe):

## What is the current behavior?

<!-- Please describe the current behavior that you are modifying, or
link to a relevant issue. -->

Issue Number: N/A

## Coderabbit Summary

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added messages for verification code requests, expiration, and
mismatches.
  - Introduced functionality to resend verification codes.
- Enhanced error handling for account confirmation and verification code
processes.

- **Improvements**
- Updated UI to handle and display messages related to code resend
status.

- **Bug Fixes**
- Improved error handling for code mismatches and expired codes during
account confirmation.

- **Configuration**
  - Added `build_in_source` parameter to Lambda function configurations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

## Does this introduce a breaking change?

- [ ] Yes
- [ ] No

<!-- If this does introduce a breaking change, please describe the
impact and migration path for existing applications below. -->

## Other information

<!-- Any other information that is important to this PR, such as
screenshots of how the component looks before and after the change. -->
  • Loading branch information
JoeKarow authored May 24, 2024
1 parent 3eac061 commit e4e49f8
Show file tree
Hide file tree
Showing 12 changed files with 150 additions and 30 deletions.
5 changes: 5 additions & 0 deletions apps/app/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
"close": "Close",
"confirm-account": {
"click-verify": "Click here to verify your confirm your InReach account",
"code-requested": "Verification code requested!",
"code-resent": "A new code has been requested. Please check your email.",
"message": "Click the following link to confirm your account:",
"subject": "Confirm your account"
},
Expand Down Expand Up @@ -114,7 +116,10 @@
"404-title": "404: Page not found.",
"500-body": "We're sorry, something went wrong with our server. Please try again later, or start a search below to find safe, verified LGBTQ+ resources in your area.",
"500-title": "500: Something went wrong.",
"code-expired": "The code provided has expired.",
"code-mismatch": "The code provided is not valid.",
"oh-no": "Oh no!",
"resend-code": "Request a new code",
"try-again-text": "<Text>Something went wrong! Please try again.</Text>"
},
"exclude": "Exclude",
Expand Down
2 changes: 2 additions & 0 deletions lambdas/cognito-messaging/samconfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ region = "us-east-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
image_repositories = []
[default.build.parameters]
build_in_source = true
2 changes: 2 additions & 0 deletions lambdas/cognito-user-migrate/samconfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ region = "us-east-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
image_repositories = []
[default.build.parameters]
build_in_source = true
7 changes: 7 additions & 0 deletions packages/api/router/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,11 @@ export const userRouter = defineRouter({
)
return handler({ input, ctx })
}),
resendCode: publicProcedure.input(schema.ZResendCodeSchema).mutation(async (opts) => {
const handler = await importHandler(
namespaced('resendCode'),
() => import('./mutation.resendCode.handler')
)
return handler(opts)
}),
})
60 changes: 38 additions & 22 deletions packages/api/router/user/mutation.confirmAccount.handler.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,49 @@
import { confirmAccount as cognitoConfirmAccount } from '@weareinreach/auth/confirmAccount'
import { TRPCError } from '@trpc/server'

import {
CodeMismatchException,
confirmAccount as cognitoConfirmAccount,
ExpiredCodeException,
} from '@weareinreach/auth/confirmAccount'
import { prisma } from '@weareinreach/db'
import { type TRPCHandlerParams } from '~api/types/handler'

import { type TConfirmAccountSchema } from './mutation.confirmAccount.schema'

const confirmAccount = async ({ input }: TRPCHandlerParams<TConfirmAccountSchema>) => {
const { code, email } = input
const response = await cognitoConfirmAccount(email, code)
try {
const { code, email } = input
const response = await cognitoConfirmAccount(email, code)

const { id } = await prisma.user.findFirstOrThrow({
where: {
email: {
equals: email.toLowerCase(),
mode: 'insensitive',
const { id } = await prisma.user.findFirstOrThrow({
where: {
email: {
equals: email.toLowerCase(),
mode: 'insensitive',
},
},
select: {
id: true,
},
},
select: {
id: true,
},
})
})

await prisma.user.update({
where: {
id,
},
data: {
emailVerified: new Date(),
},
})
return response
await prisma.user.update({
where: {
id,
},
data: {
emailVerified: new Date(),
},
})
return response
} catch (error) {
if (error instanceof CodeMismatchException) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Code mismatch', cause: error })
}
if (error instanceof ExpiredCodeException) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Code expired', cause: error })
}
throw error
}
}
export default confirmAccount
15 changes: 15 additions & 0 deletions packages/api/router/user/mutation.resendCode.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { resendVerificationCode } from '@weareinreach/auth/lib/resendCode'
import { handleError } from '~api/lib/errorHandler'
import { type TRPCHandlerParams } from '~api/types/handler'

import { type TResendCodeSchema } from './mutation.resendCode.schema'

const resendCode = async ({ input }: TRPCHandlerParams<TResendCodeSchema>) => {
try {
const result = await resendVerificationCode(input.email)
return result
} catch (error) {
return handleError(error)
}
}
export default resendCode
14 changes: 14 additions & 0 deletions packages/api/router/user/mutation.resendCode.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z } from 'zod'

import { decodeUrl } from '~api/lib/encodeUrl'

export const ZResendCodeSchema = z.union([
z.object({ email: z.string().email().toLowerCase(), data: z.never().optional() }),
z
.object({ data: z.string(), email: z.never().optional() })
.transform(({ data }) => ({ email: decodeUrl(data).email })),
])

// .object({ email: z.string().email().toLowerCase() })
// .or(z.object({ data: z.string() }))
export type TResendCodeSchema = z.infer<typeof ZResendCodeSchema>
1 change: 1 addition & 0 deletions packages/api/router/user/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './mutation.confirmAccount.schema'
export * from './mutation.create.schema'
export * from './mutation.deleteAccount.schema'
export * from './mutation.forgotPassword.schema'
export * from './mutation.resendCode.schema'
export * from './mutation.resetPassword.schema'
export * from './mutation.submitSurvey.schema'
// codegen:end
4 changes: 3 additions & 1 deletion packages/auth/lib/cognitoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { z } from 'zod'
import { createHmac } from 'crypto'

import { prisma } from '@weareinreach/db'
import { getEnv } from '@weareinreach/env'
import { getEnv, isLocalDev } from '@weareinreach/env'
import { createLoggerInstance } from '@weareinreach/util/logger'

import { decodeCognitoIdJwt } from './cognitoJwt'
Expand All @@ -33,6 +33,8 @@ export const cognito = new CognitoIdentityProvider({
secretAccessKey: getEnv('COGNITO_SECRET'),
},
logger,
// eslint-disable-next-line node/no-process-env
...(isLocalDev && { endpoint: process.env.COGNITO_LOCAL_ENDPOINT }),
})
export const ClientId = getEnv('COGNITO_CLIENT_ID')

Expand Down
2 changes: 2 additions & 0 deletions packages/auth/lib/confirmAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export const confirmAccount = async (email: string, code: string) => {
})
return response
}

export { ExpiredCodeException, CodeMismatchException } from '@aws-sdk/client-cognito-identity-provider'
5 changes: 3 additions & 2 deletions packages/env/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,6 @@ export const env = createEnv({

export const getEnv = <T extends keyof typeof env>(envVar: T): (typeof env)[T] => env[envVar]

export const isDev = process.env.NODE_ENV === 'development'
export const isVercelProd = process.env.VERCEL_ENV === 'production'
export * from './checks'
// export const isDev = process.env.NODE_ENV === 'development'
// export const isVercelProd = process.env.VERCEL_ENV === 'production'
63 changes: 58 additions & 5 deletions packages/ui/modals/AccountVerified.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { useDisclosure } from '@mantine/hooks'
import { useRouter } from 'next/router'
import { Trans, useTranslation } from 'next-i18next'
import { forwardRef, useEffect, useMemo, useState } from 'react'
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'
import { z } from 'zod'

import { decodeUrl } from '@weareinreach/api/lib/encodeUrl'
Expand Down Expand Up @@ -44,10 +44,27 @@ const AccountVerifyModalBody = forwardRef<HTMLButtonElement, AccountVerifyModalB
const variants = useCustomVariant()
const [success, setSuccess] = useState(false)
const [error, setError] = useState(false)
const [codeSent, setCodeSent] = useState(false)
const verifyAccount = api.user.confirmAccount.useMutation({
onSuccess: () => setSuccess(true),
onError: () => setError(true),
})
const resendCode = api.user.resendCode.useMutation({
onSuccess: () => setCodeSent(true),
})

const handleResendCode = useCallback((data: string) => () => resendCode.mutate({ data }), [resendCode])

const hasError = useMemo(() => {
if (!verifyAccount.isError) {
return false
}
if (verifyAccount.error.data?.cause instanceof Error && verifyAccount.error.data?.cause?.name) {
return verifyAccount.error.data.cause.name
}
return 'UnknownError'
}, [verifyAccount.error?.data?.cause, verifyAccount.isError])

// const DataSchema = z.string().default('')
const [opened, handler] = useDisclosure(autoOpen)
const { isMobile } = useScreenSize()
Expand Down Expand Up @@ -137,19 +154,52 @@ const AccountVerifyModalBody = forwardRef<HTMLButtonElement, AccountVerifyModalB
),
[t, variants, handler]
)
const bodyError = useMemo(
() => (

const errorI18nKey = useMemo(() => {
switch (hasError) {
case 'NotAuthorizedException':
case 'CodeMismatchException': {
return 'errors.code-mismatch'
}
case 'ExpiredCodeException': {
return 'errors.code-expired'
}
default: {
return 'errors.try-again-text'
}
}
}, [hasError])

const bodyError = useMemo(() => {
return (
<Stack align='center' spacing={24}>
<Stack spacing={0} align='center'>
<Title order={1}>🫣</Title>
<Title order={2}>{t('errors.oh-no')}</Title>
</Stack>
<Trans
i18nKey='errors.try-again-text'
i18nKey={errorI18nKey}
components={{
Text: <Text variant={variants.Text.utility1darkGray}>.</Text>,
}}
/>
{errorI18nKey !== 'errors.try-again-text' && parsedData.data && (
<Button variant={variants.Button.primarySm} onClick={handleResendCode(parsedData.data.data)}>
{t('errors.resend-code')}
</Button>
)}
</Stack>
)
}, [errorI18nKey, t, variants, handleResendCode, parsedData.data])

const bodyCodeResent = useMemo(
() => (
<Stack align='center' spacing={24}>
<Stack spacing={0} align='center'>
<Title order={1}>📬</Title>
<Title order={2}>{t('confirm-account.code-requested')}</Title>
</Stack>
<Text variant={variants.Text.utility1darkGray}>{t('confirm-account.code-resent')}</Text>
</Stack>
),
[t, variants]
Expand All @@ -159,11 +209,14 @@ const AccountVerifyModalBody = forwardRef<HTMLButtonElement, AccountVerifyModalB
if (success) {
return bodySuccess
}
if (codeSent) {
return bodyCodeResent
}
if (error) {
return bodyError
}
return bodyWorking
}, [bodyError, bodySuccess, bodyWorking, error, success])
}, [bodyCodeResent, bodyError, bodySuccess, bodyWorking, codeSent, error, success])

return (
<Modal title={modalTitle} opened={opened} onClose={handler.close} fullScreen={isMobile}>
Expand Down

0 comments on commit e4e49f8

Please sign in to comment.