Implement API call to link SAS account * Add endpoint to actually link SAS account linking * add logging of error * Refactor tocDate to getCurrentDateWithoutTime Approved-by: Joakim Jäderberg
171 lines
4.3 KiB
TypeScript
171 lines
4.3 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`,
|
|
}
|
|
}
|
|
|
|
switch (res.linkingState) {
|
|
case "alreadyLinked":
|
|
return {
|
|
url: `/${lang}/sas-x-scandic/error?errorCode=alreadyLinked`,
|
|
type: "replace",
|
|
}
|
|
case "linked":
|
|
return {
|
|
url: `/${lang}/sas-x-scandic/link/success`,
|
|
type: "replace",
|
|
}
|
|
case "dateOfBirthMismatch":
|
|
return {
|
|
url: `/${lang}/sas-x-scandic/error?errorCode=dateOfBirthMismatch`,
|
|
type: "replace",
|
|
}
|
|
case "error":
|
|
return {
|
|
url: `/${lang}/sas-x-scandic/error`,
|
|
type: "replace",
|
|
}
|
|
}
|
|
}
|