Merged in feat/sas-otp-error-handling (pull request #1272)
Feat/sas otp error handling * Improve error handling for SAS OTP * Remove failing and deprecated test Approved-by: Joakim Jäderberg
This commit is contained in:
@@ -82,13 +82,23 @@ export async function GET(
|
|||||||
const [data, error] = await safeTry(
|
const [data, error] = await safeTry(
|
||||||
serverClient().partner.sas.requestOtp({})
|
serverClient().partner.sas.requestOtp({})
|
||||||
)
|
)
|
||||||
// status: 'SENT' => OK
|
|
||||||
if (!data || error) {
|
if (!data || error) {
|
||||||
//TODO: Check what error we get
|
|
||||||
console.error("[SAS] Failed to request OTP", error)
|
console.error("[SAS] Failed to request OTP", error)
|
||||||
redirect(`/${lang}/sas-x-scandic/error`)
|
redirect(`/${lang}/sas-x-scandic/error`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (data.status) {
|
||||||
|
case "ABUSED":
|
||||||
|
redirect(`/${params.lang}/sas-x-scandic/error?errorCode=tooManyCodes`)
|
||||||
|
case "NOTSENT":
|
||||||
|
redirect(`/${params.lang}/sas-x-scandic/error`)
|
||||||
|
case "NULL":
|
||||||
|
case "RETRY":
|
||||||
|
case "EXPIRED":
|
||||||
|
// These errors should never happen for request, but according to the API spec they can
|
||||||
|
throw new Error(`Unhandled request OTP status ${data.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
console.log("[SAS] Request OTP response", data)
|
console.log("[SAS] Request OTP response", data)
|
||||||
|
|
||||||
const otpUrl = new URL(
|
const otpUrl = new URL(
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
|
||||||
|
import { GenericError } from "./GenericError"
|
||||||
|
|
||||||
|
export function TooManyFailedAttemptsError() {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericError
|
||||||
|
title={intl.formatMessage({ id: "Too many failed attempts." })}
|
||||||
|
variant="info"
|
||||||
|
>
|
||||||
|
<Body textAlign="center">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "Please wait 1 hour before trying again.",
|
||||||
|
})}
|
||||||
|
</Body>
|
||||||
|
<Button theme="base" disabled>
|
||||||
|
{intl.formatMessage({ id: "Send new code" })}
|
||||||
|
</Button>
|
||||||
|
</GenericError>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
|
|
||||||
|
import { AlreadyLinkedError } from "../components/AlreadyLinkedError"
|
||||||
import { DateOfBirthError } from "../components/DateOfBirthError"
|
import { DateOfBirthError } from "../components/DateOfBirthError"
|
||||||
import { GenericError } from "../components/GenericError"
|
import { GenericError } from "../components/GenericError"
|
||||||
import { SASModalContactBlock } from "../components/SASModal"
|
import { SASModalContactBlock } from "../components/SASModal"
|
||||||
|
import { TooManyCodesError } from "../components/TooManyCodesError"
|
||||||
|
import { TooManyFailedAttemptsError } from "../components/TooManyFailedAttemptsError"
|
||||||
|
|
||||||
import type { LangParams, PageArgs, SearchParams } from "@/types/params"
|
import type { LangParams, PageArgs, SearchParams } from "@/types/params"
|
||||||
|
|
||||||
@@ -13,10 +16,24 @@ export default async function Page({
|
|||||||
}: PageArgs<LangParams> & SearchParams<{ errorCode?: "dateOfBirthMismatch" }>) {
|
}: PageArgs<LangParams> & SearchParams<{ errorCode?: "dateOfBirthMismatch" }>) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|
||||||
if (searchParams.errorCode === "dateOfBirthMismatch") {
|
const { errorCode } = searchParams
|
||||||
|
|
||||||
|
if (errorCode === "dateOfBirthMismatch") {
|
||||||
return <DateOfBirthError />
|
return <DateOfBirthError />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (errorCode === "tooManyFailedAttempts") {
|
||||||
|
return <TooManyFailedAttemptsError />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorCode === "tooManyCodes") {
|
||||||
|
return <TooManyCodesError />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorCode === "alreadyLinked") {
|
||||||
|
return <AlreadyLinkedError />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericError
|
<GenericError
|
||||||
title={intl.formatMessage({
|
title={intl.formatMessage({
|
||||||
|
|||||||
@@ -31,9 +31,11 @@ export default async function SASxScandicLinkPage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return alreadyLinked ? (
|
if (alreadyLinked) {
|
||||||
<AlreadyLinkedError />
|
redirect(`/${params.lang}/sas-x-scandic/error?errorCode=alreadyLinked`)
|
||||||
) : (
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<SASModal>
|
<SASModal>
|
||||||
<LinkAccountForm
|
<LinkAccountForm
|
||||||
initialDateOfBirth={profile?.dateOfBirth ?? null}
|
initialDateOfBirth={profile?.dateOfBirth ?? null}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { cx } from "class-variance-authority"
|
import { cx } from "class-variance-authority"
|
||||||
import { OTPInput, type SlotProps } from "input-otp"
|
import { OTPInput, type SlotProps } from "input-otp"
|
||||||
|
import { useParams, useRouter } from "next/navigation"
|
||||||
import { type ReactNode, useState, useTransition } from "react"
|
import { type ReactNode, useState, useTransition } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
@@ -13,7 +14,6 @@ import Body from "@/components/TempDesignSystem/Text/Body"
|
|||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
|
||||||
|
|
||||||
import { GenericError } from "../components/GenericError"
|
import { GenericError } from "../components/GenericError"
|
||||||
import { SASModal, SASModalContactBlock } from "../components/SASModal"
|
import { SASModal, SASModalContactBlock } from "../components/SASModal"
|
||||||
@@ -22,7 +22,7 @@ import Loading from "./loading"
|
|||||||
import styles from "./OneTimePasswordForm.module.css"
|
import styles from "./OneTimePasswordForm.module.css"
|
||||||
|
|
||||||
import type { RequestOtpError } from "@/server/routers/partners/sas/otp/request/requestOtpError"
|
import type { RequestOtpError } from "@/server/routers/partners/sas/otp/request/requestOtpError"
|
||||||
import type { VerifyOtpError } from "@/server/routers/partners/sas/otp/verify/verifyOtpError"
|
import type { OtpError } from "./page"
|
||||||
|
|
||||||
export default function OneTimePasswordForm({
|
export default function OneTimePasswordForm({
|
||||||
heading,
|
heading,
|
||||||
@@ -37,8 +37,10 @@ export default function OneTimePasswordForm({
|
|||||||
footnote?: string | ReactNode
|
footnote?: string | ReactNode
|
||||||
otpLength: number
|
otpLength: number
|
||||||
onSubmit: (args: { otp: string }) => Promise<void>
|
onSubmit: (args: { otp: string }) => Promise<void>
|
||||||
error?: ReactNode
|
error?: OtpError
|
||||||
}) {
|
}) {
|
||||||
|
const router = useRouter()
|
||||||
|
const params = useParams()
|
||||||
const [isPending, startTransition] = useTransition()
|
const [isPending, startTransition] = useTransition()
|
||||||
const [disableResend, setDisableResend] = useState(false)
|
const [disableResend, setDisableResend] = useState(false)
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
@@ -64,6 +66,20 @@ export default function OneTimePasswordForm({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (requestOtp.data?.status) {
|
||||||
|
case "ABUSED":
|
||||||
|
router.push(`/${params.lang}/sas-x-scandic/error?errorCode=tooManyCodes`)
|
||||||
|
return <Loading />
|
||||||
|
case "NOTSENT":
|
||||||
|
router.push(`/${params.lang}/sas-x-scandic/error`)
|
||||||
|
return <Loading />
|
||||||
|
case "NULL":
|
||||||
|
case "RETRY":
|
||||||
|
case "EXPIRED":
|
||||||
|
// These errors should never happen for request, but according to the API spec they can
|
||||||
|
throw new Error(`Unhandled request OTP status ${requestOtp.data?.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
function handleRequestNewOtp(event: React.MouseEvent) {
|
function handleRequestNewOtp(event: React.MouseEvent) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (disableResend) return
|
if (disableResend) return
|
||||||
@@ -84,6 +100,35 @@ export default function OneTimePasswordForm({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getResendOtpLink = (str: ReactNode) => (
|
||||||
|
<Link
|
||||||
|
href="#"
|
||||||
|
onClick={handleRequestNewOtp}
|
||||||
|
color="red"
|
||||||
|
variant="default"
|
||||||
|
size="tiny"
|
||||||
|
className={disableResend ? styles["disabled-link"] : ""}
|
||||||
|
>
|
||||||
|
{str}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
|
||||||
|
const errorMessages: Record<OtpError, ReactNode> = {
|
||||||
|
invalidCode: intl.formatMessage({
|
||||||
|
id: "The code you’ve entered is incorrect.",
|
||||||
|
}),
|
||||||
|
expiredCode: intl.formatMessage<ReactNode>(
|
||||||
|
{
|
||||||
|
id: "The code you’ve entered have expired. <resendOtpLink>Resend code.</resendOtpLink>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resendOtpLink: getResendOtpLink,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage = error ? errorMessages[error] : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SASModal>
|
<SASModal>
|
||||||
<Subtitle textAlign={"center"}>{heading}</Subtitle>
|
<Subtitle textAlign={"center"}>{heading}</Subtitle>
|
||||||
@@ -110,10 +155,10 @@ export default function OneTimePasswordForm({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{error && (
|
{errorMessage && (
|
||||||
<div className={styles["error-message"]}>
|
<div className={styles["error-message"]}>
|
||||||
<ErrorCircleFilledIcon height={20} width={20} color="red" />
|
<ErrorCircleFilledIcon height={20} width={20} color="red" />
|
||||||
<Caption color="red">{error}</Caption>
|
<Caption color="red">{errorMessage}</Caption>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
@@ -124,18 +169,7 @@ export default function OneTimePasswordForm({
|
|||||||
id: "Didn't receive a code? <resendOtpLink>Resend code</resendOtpLink>",
|
id: "Didn't receive a code? <resendOtpLink>Resend code</resendOtpLink>",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resendOtpLink: (str) => (
|
resendOtpLink: getResendOtpLink,
|
||||||
<Link
|
|
||||||
href="#"
|
|
||||||
onClick={handleRequestNewOtp}
|
|
||||||
color="red"
|
|
||||||
variant="default"
|
|
||||||
size="tiny"
|
|
||||||
className={disableResend ? styles["disabled-link"] : ""}
|
|
||||||
>
|
|
||||||
{str}
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</Footnote>
|
</Footnote>
|
||||||
@@ -176,23 +210,3 @@ const getRequestErrorBody = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getVerifyErrorBody = (
|
|
||||||
intl: ReturnType<typeof useIntl>,
|
|
||||||
errorCode: VerifyOtpError["errorCode"]
|
|
||||||
) => {
|
|
||||||
switch (errorCode) {
|
|
||||||
case "WRONG_OTP":
|
|
||||||
return intl.formatMessage({
|
|
||||||
id: "The code you entered is incorrect. Please try again.",
|
|
||||||
})
|
|
||||||
case "OTP_EXPIRED":
|
|
||||||
return intl.formatMessage({
|
|
||||||
id: "OTP has expired. Please try again.",
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return intl.formatMessage({
|
|
||||||
id: "An error occurred while requesting a new OTP",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ import OneTimePasswordForm from "./OneTimePasswordForm"
|
|||||||
import type { LangParams, PageArgs, SearchParams } from "@/types/params"
|
import type { LangParams, PageArgs, SearchParams } from "@/types/params"
|
||||||
import type { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
const otpError = z.enum(["invalidCode", "expiredCode"])
|
||||||
const searchParamsSchema = z.object({
|
const searchParamsSchema = z.object({
|
||||||
intent: z.enum(["link"]),
|
intent: z.enum(["link"]),
|
||||||
to: z.string(),
|
to: z.string(),
|
||||||
error: z.enum(["invalidCode"]).optional(),
|
error: otpError.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export type OtpError = z.infer<typeof otpError>
|
||||||
|
|
||||||
export default async function SASxScandicOneTimePasswordPage({
|
export default async function SASxScandicOneTimePasswordPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
params,
|
params,
|
||||||
@@ -37,37 +40,48 @@ export default async function SASxScandicOneTimePasswordPage({
|
|||||||
redirect(`/${params.lang}/sas-x-scandic/login?intent=${intent}`)
|
redirect(`/${params.lang}/sas-x-scandic/login?intent=${intent}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors = {
|
|
||||||
invalidCode: intl.formatMessage({
|
|
||||||
id: "The code you’ve entered is incorrect.",
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleOtpVerified({ otp }: { otp: string }) {
|
async function handleOtpVerified({ otp }: { otp: string }) {
|
||||||
"use server"
|
"use server"
|
||||||
|
|
||||||
const [data, error] = await safeTry(
|
const [data, error] = await safeTry(
|
||||||
serverClient().partner.sas.verifyOtp({ otp })
|
serverClient().partner.sas.verifyOtp({ otp })
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO correct status?
|
if (error || !data) {
|
||||||
// TODO handle all errors
|
throw error || new Error("OTP verification failed")
|
||||||
// STATUS === VERIFIED => ok
|
}
|
||||||
// STATUS === ABUSED => otpRetryCount > otpMaxRetryCount
|
|
||||||
if (error || data?.status !== "VERIFIED") {
|
|
||||||
const search = new URLSearchParams({
|
|
||||||
...searchParams,
|
|
||||||
error: "invalidCode",
|
|
||||||
}).toString()
|
|
||||||
|
|
||||||
redirect(`/${params.lang}/sas-x-scandic/otp?${search}`)
|
switch (data.status) {
|
||||||
|
case "ABUSED":
|
||||||
|
redirect(
|
||||||
|
`/${params.lang}/sas-x-scandic/error?errorCode=tooManyFailedAttempts`
|
||||||
|
)
|
||||||
|
case "EXPIRED": {
|
||||||
|
const search = new URLSearchParams({
|
||||||
|
...searchParams,
|
||||||
|
error: "expiredCode",
|
||||||
|
}).toString()
|
||||||
|
|
||||||
|
redirect(`/${params.lang}/sas-x-scandic/otp?${search}`)
|
||||||
|
}
|
||||||
|
case "RETRY": {
|
||||||
|
const search = new URLSearchParams({
|
||||||
|
...searchParams,
|
||||||
|
error: "invalidCode",
|
||||||
|
}).toString()
|
||||||
|
|
||||||
|
redirect(`/${params.lang}/sas-x-scandic/otp?${search}`)
|
||||||
|
}
|
||||||
|
case "NOTSENT":
|
||||||
|
case "NULL":
|
||||||
|
case "SENT":
|
||||||
|
case "PENDING":
|
||||||
|
// These errors should never happen for verify, but according to the API spec they can
|
||||||
|
throw new Error("Unhandled OTP status")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (intent) {
|
switch (intent) {
|
||||||
case "link":
|
case "link":
|
||||||
return handleLinkAccount({ lang: params.lang })
|
return handleLinkAccount({ lang: params.lang })
|
||||||
default:
|
|
||||||
throw new Error("")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +107,7 @@ export default async function SASxScandicOneTimePasswordPage({
|
|||||||
})}
|
})}
|
||||||
otpLength={6}
|
otpLength={6}
|
||||||
onSubmit={handleOtpVerified}
|
onSubmit={handleOtpVerified}
|
||||||
error={error ? errors[error] : undefined}
|
error={error}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -523,6 +523,7 @@
|
|||||||
"Terms and conditions": "Terms and conditions",
|
"Terms and conditions": "Terms and conditions",
|
||||||
"Thank you": "Thank you",
|
"Thank you": "Thank you",
|
||||||
"Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please <emailLink>contact us.</emailLink>": "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please <emailLink>contact us.</emailLink>",
|
"Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please <emailLink>contact us.</emailLink>": "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please <emailLink>contact us.</emailLink>",
|
||||||
|
"The code you’ve entered have expired. <resendOtpLink>Resend code.</resendOtpLink>": "The code you’ve entered have expired. <resendOtpLink>Resend code.</resendOtpLink>",
|
||||||
"The code you’ve entered is incorrect.": "The code you’ve entered is incorrect.",
|
"The code you’ve entered is incorrect.": "The code you’ve entered is incorrect.",
|
||||||
"The new price is": "The new price is",
|
"The new price is": "The new price is",
|
||||||
"The price has increased": "The price has increased",
|
"The price has increased": "The price has increased",
|
||||||
|
|||||||
@@ -18,7 +18,16 @@ import type { OtpState } from "../getOTPState"
|
|||||||
const inputSchema = z.object({})
|
const inputSchema = z.object({})
|
||||||
|
|
||||||
const outputSchema = z.object({
|
const outputSchema = z.object({
|
||||||
status: z.string(),
|
status: z.enum([
|
||||||
|
"VERIFIED",
|
||||||
|
"ABUSED",
|
||||||
|
"EXPIRED",
|
||||||
|
"PENDING",
|
||||||
|
"RETRY",
|
||||||
|
"SENT",
|
||||||
|
"NULL",
|
||||||
|
"NOTSENT",
|
||||||
|
]),
|
||||||
referenceId: z.string().uuid(),
|
referenceId: z.string().uuid(),
|
||||||
databaseUUID: z.string().uuid(),
|
databaseUUID: z.string().uuid(),
|
||||||
otpExpiration: z.number(),
|
otpExpiration: z.number(),
|
||||||
@@ -42,14 +51,15 @@ export const requestOtp = protectedProcedure
|
|||||||
tokenResponse.status,
|
tokenResponse.status,
|
||||||
tokenResponse.statusText
|
tokenResponse.statusText
|
||||||
)
|
)
|
||||||
if (!tokenResponse.ok) {
|
|
||||||
const errorBody = await tokenResponse.json()
|
|
||||||
console.error("[SAS] requestOtp error", errorBody)
|
|
||||||
throw createError(errorBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseResult = outputSchema.safeParse(await tokenResponse.json())
|
const body = await tokenResponse.json()
|
||||||
|
const parseResult = outputSchema.safeParse(body)
|
||||||
if (!parseResult.success) {
|
if (!parseResult.success) {
|
||||||
|
console.error("[SAS] requestOtp error", body)
|
||||||
|
|
||||||
|
if (!tokenResponse.ok) {
|
||||||
|
throw createError(body)
|
||||||
|
}
|
||||||
throw createError(parseResult.error)
|
throw createError(parseResult.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ import { parseSASRequestOtpError } from "./requestOtpError"
|
|||||||
|
|
||||||
describe("requestOtpError", () => {
|
describe("requestOtpError", () => {
|
||||||
it("parses error with invalid error code", () => {
|
it("parses error with invalid error code", () => {
|
||||||
const error = {
|
|
||||||
status: "status",
|
|
||||||
error: "error",
|
|
||||||
errorCode: "a",
|
|
||||||
databaseUUID: "9ffefefe-df0e-4229-9792-5ed31bef1db4",
|
|
||||||
}
|
|
||||||
|
|
||||||
const actual = parseSASRequestOtpError({
|
const actual = parseSASRequestOtpError({
|
||||||
status: "status",
|
status: "status",
|
||||||
error: "error",
|
error: "error",
|
||||||
@@ -21,17 +14,4 @@ describe("requestOtpError", () => {
|
|||||||
errorCode: "UNKNOWN",
|
errorCode: "UNKNOWN",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("parses error as TOO_MANY_REQUESTS error code", () => {
|
|
||||||
const actual = parseSASRequestOtpError({
|
|
||||||
status: "status",
|
|
||||||
error: "error",
|
|
||||||
errorCode: 10,
|
|
||||||
databaseUUID: "9ffefefe-df0e-4229-9792-5ed31bef1db4",
|
|
||||||
otpExpiration: "2021-09-01T00:00:00Z",
|
|
||||||
})
|
|
||||||
expect(actual).toEqual({
|
|
||||||
errorCode: "TOO_MANY_REQUESTS",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -51,7 +51,16 @@ const getErrorCodeByNumber = (number: number): RequestOtpResponseError => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sasOtpRequestErrorSchema = z.object({
|
const sasOtpRequestErrorSchema = z.object({
|
||||||
status: z.string(),
|
status: z.enum([
|
||||||
|
"VERIFIED",
|
||||||
|
"ABUSED",
|
||||||
|
"EXPIRED",
|
||||||
|
"PENDING",
|
||||||
|
"RETRY",
|
||||||
|
"SENT",
|
||||||
|
"NULL",
|
||||||
|
"NOTSENT",
|
||||||
|
]),
|
||||||
otpExpiration: z.string().datetime(),
|
otpExpiration: z.string().datetime(),
|
||||||
error: z.string(),
|
error: z.string(),
|
||||||
errorCode: z.number(),
|
errorCode: z.number(),
|
||||||
|
|||||||
@@ -17,7 +17,16 @@ const inputSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const outputSchema = z.object({
|
const outputSchema = z.object({
|
||||||
status: z.string(), // TODO: Change to enum
|
status: z.enum([
|
||||||
|
"VERIFIED",
|
||||||
|
"ABUSED",
|
||||||
|
"EXPIRED",
|
||||||
|
"PENDING",
|
||||||
|
"RETRY",
|
||||||
|
"SENT",
|
||||||
|
"NULL",
|
||||||
|
"NOTSENT",
|
||||||
|
]),
|
||||||
referenceId: z.string().uuid(),
|
referenceId: z.string().uuid(),
|
||||||
databaseUUID: z.string().uuid().optional(),
|
databaseUUID: z.string().uuid().optional(),
|
||||||
})
|
})
|
||||||
@@ -29,7 +38,6 @@ export const verifyOtp = protectedProcedure
|
|||||||
const sasAuthToken = getSasToken()
|
const sasAuthToken = getSasToken()
|
||||||
|
|
||||||
if (!sasAuthToken) {
|
if (!sasAuthToken) {
|
||||||
// TODO: Should we verify that the SAS token isn't expired?
|
|
||||||
throw createError("AUTH_TOKEN_NOT_FOUND")
|
throw createError("AUTH_TOKEN_NOT_FOUND")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user