diff --git a/apps/app/public/locales/en/common.json b/apps/app/public/locales/en/common.json index 44f81f944e..358796651c 100644 --- a/apps/app/public/locales/en/common.json +++ b/apps/app/public/locales/en/common.json @@ -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" }, @@ -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": "Something went wrong! Please try again." }, "exclude": "Exclude", diff --git a/lambdas/cognito-messaging/samconfig.toml b/lambdas/cognito-messaging/samconfig.toml index c248d9ce6c..1eee0fde4c 100644 --- a/lambdas/cognito-messaging/samconfig.toml +++ b/lambdas/cognito-messaging/samconfig.toml @@ -7,3 +7,5 @@ region = "us-east-1" confirm_changeset = true capabilities = "CAPABILITY_IAM" image_repositories = [] +[default.build.parameters] +build_in_source = true diff --git a/lambdas/cognito-user-migrate/samconfig.toml b/lambdas/cognito-user-migrate/samconfig.toml index 90025a6f6a..09c01f2092 100644 --- a/lambdas/cognito-user-migrate/samconfig.toml +++ b/lambdas/cognito-user-migrate/samconfig.toml @@ -7,3 +7,5 @@ region = "us-east-1" confirm_changeset = true capabilities = "CAPABILITY_IAM" image_repositories = [] +[default.build.parameters] +build_in_source = true diff --git a/packages/api/router/user/index.ts b/packages/api/router/user/index.ts index 68580cc556..274f0cbf79 100644 --- a/packages/api/router/user/index.ts +++ b/packages/api/router/user/index.ts @@ -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) + }), }) diff --git a/packages/api/router/user/mutation.confirmAccount.handler.ts b/packages/api/router/user/mutation.confirmAccount.handler.ts index c1f0e1a30f..e77a287549 100644 --- a/packages/api/router/user/mutation.confirmAccount.handler.ts +++ b/packages/api/router/user/mutation.confirmAccount.handler.ts @@ -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) => { - 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 diff --git a/packages/api/router/user/mutation.resendCode.handler.ts b/packages/api/router/user/mutation.resendCode.handler.ts new file mode 100644 index 0000000000..ce6cf255e5 --- /dev/null +++ b/packages/api/router/user/mutation.resendCode.handler.ts @@ -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) => { + try { + const result = await resendVerificationCode(input.email) + return result + } catch (error) { + return handleError(error) + } +} +export default resendCode diff --git a/packages/api/router/user/mutation.resendCode.schema.ts b/packages/api/router/user/mutation.resendCode.schema.ts new file mode 100644 index 0000000000..1f4b082a81 --- /dev/null +++ b/packages/api/router/user/mutation.resendCode.schema.ts @@ -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 diff --git a/packages/api/router/user/schemas.ts b/packages/api/router/user/schemas.ts index 9e9b23b60e..b8ea8beeb3 100644 --- a/packages/api/router/user/schemas.ts +++ b/packages/api/router/user/schemas.ts @@ -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 diff --git a/packages/auth/lib/cognitoClient.ts b/packages/auth/lib/cognitoClient.ts index e89e0e83d6..07b52c8310 100644 --- a/packages/auth/lib/cognitoClient.ts +++ b/packages/auth/lib/cognitoClient.ts @@ -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' @@ -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') diff --git a/packages/auth/lib/confirmAccount.ts b/packages/auth/lib/confirmAccount.ts index bdd6e983d3..0f7f2516c5 100644 --- a/packages/auth/lib/confirmAccount.ts +++ b/packages/auth/lib/confirmAccount.ts @@ -9,3 +9,5 @@ export const confirmAccount = async (email: string, code: string) => { }) return response } + +export { ExpiredCodeException, CodeMismatchException } from '@aws-sdk/client-cognito-identity-provider' diff --git a/packages/env/index.ts b/packages/env/index.ts index d27a30ebf7..5885ad1040 100644 --- a/packages/env/index.ts +++ b/packages/env/index.ts @@ -90,5 +90,6 @@ export const env = createEnv({ export const getEnv = (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' diff --git a/packages/ui/modals/AccountVerified.tsx b/packages/ui/modals/AccountVerified.tsx index 4f6cb47c00..50d943e549 100644 --- a/packages/ui/modals/AccountVerified.tsx +++ b/packages/ui/modals/AccountVerified.tsx @@ -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' @@ -44,10 +44,27 @@ const AccountVerifyModalBody = forwardRef 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() @@ -137,19 +154,52 @@ const AccountVerifyModalBody = forwardRef ( + + 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 ( 🫣 {t('errors.oh-no')} ., }} /> + {errorI18nKey !== 'errors.try-again-text' && parsedData.data && ( + + )} + + ) + }, [errorI18nKey, t, variants, handleResendCode, parsedData.data]) + + const bodyCodeResent = useMemo( + () => ( + + + 📬 + {t('confirm-account.code-requested')} + + {t('confirm-account.code-resent')} ), [t, variants] @@ -159,11 +209,14 @@ const AccountVerifyModalBody = forwardRef