Files
web/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/otp/page.tsx
2025-02-17 10:43:54 +01:00

157 lines
4.0 KiB
TypeScript

import { cookies } from "next/headers"
import { redirect } from "next/navigation"
import { z } from "zod"
import { serverClient } from "@/lib/trpc/server"
import { getIntl } from "@/i18n"
import { safeTry } from "@/utils/safeTry"
import { SAS_TOKEN_STORAGE_KEY } from "../sasUtils"
import OneTimePasswordForm, {
type OnSubmitHandler,
} from "./OneTimePasswordForm"
import type { LangParams, PageArgs, SearchParams } from "@/types/params"
import type { Lang } from "@/constants/languages"
const otpError = z.enum(["invalidCode", "expiredCode"])
const searchParamsSchema = z.object({
intent: z.enum(["link"]),
to: z.string(),
error: otpError.optional(),
})
export type OtpError = z.infer<typeof otpError>
export default async function SASxScandicOneTimePasswordPage({
searchParams,
params,
}: PageArgs<LangParams> & SearchParams) {
const intl = await getIntl()
const cookieStore = cookies()
const tokenCookie = cookieStore.get(SAS_TOKEN_STORAGE_KEY)
const result = searchParamsSchema.safeParse(searchParams)
if (!result.success) {
throw new Error("Invalid search params")
}
const { intent, to, error } = result.data
if (!verifyTokenValidity(tokenCookie?.value)) {
redirect(`/${params.lang}/sas-x-scandic/login?intent=${intent}`)
}
const handleOtpVerified: OnSubmitHandler = async ({ otp }) => {
"use server"
const [data, error] = await safeTry(
serverClient().partner.sas.verifyOtp({ otp })
)
if (error || !data) {
throw error || new Error("OTP verification failed")
}
switch (data.status) {
case "ABUSED":
return {
url: `/${params.lang}/sas-x-scandic/error?errorCode=tooManyFailedAttempts`,
}
case "EXPIRED": {
const search = new URLSearchParams({
...searchParams,
error: "expiredCode",
}).toString()
return {
url: `/${params.lang}/sas-x-scandic/otp?${search}`,
}
}
case "RETRY": {
const search = new URLSearchParams({
...searchParams,
error: "invalidCode",
}).toString()
return {
url: `/${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) {
case "link":
return handleLinkAccount({ lang: params.lang })
}
}
return (
<OneTimePasswordForm
heading={intl.formatMessage({ id: "Verification code" })}
ingress={intl.formatMessage<React.ReactNode>(
{
id: "Please enter the code sent to <maskedContactInfo></maskedContactInfo> in order to confirm your account linking.",
},
{
maskedContactInfo: () => (
<>
<br />
<strong>{to}</strong>
<br />
</>
),
}
)}
footnote={intl.formatMessage({
id: "This verifcation is needed for additional security.",
})}
otpLength={6}
onSubmit={handleOtpVerified}
error={error}
/>
)
}
function verifyTokenValidity(token: string | undefined) {
if (!token) {
return false
}
try {
const decoded = JSON.parse(atob(token.split(".")[1]))
const expiry = decoded.exp * 1000
return Date.now() < expiry
} catch {
return false
}
}
async function handleLinkAccount({
lang,
}: {
lang: Lang
}): ReturnType<OnSubmitHandler> {
const [res, error] = await safeTry(serverClient().partner.sas.linkAccount())
if (!res || error) {
console.error("[SAS] link account error", error)
return {
url: `/${lang}/sas-x-scandic/error?errorCode=link_error`,
}
}
console.log("[SAS] link account response", res)
switch (res.linkingState) {
case "linked":
return {
url: `/${lang}/sas-x-scandic/link/success`,
type: "replace",
}
}
}