First steps towards the SAS partnership * otp flow now pretends to do the linking * Update LinkAccountForm header * Update redirect times * Clean up comments * Set maxAge on sas cookies * make all SAS routes protected * Merge remote-tracking branch 'refs/remotes/origin/feature/sas-login' into feature/sas-login * Require auth for sas link flow * Fix resend otp * Add error support to OneTimePasswordForm * Add Sentry to SAS error boundary * Move SAS_REQUEST_OTP_STATE_STORAGE_COOKIE_NAME * Add missing translations * Merge branch 'master' of bitbucket.org:scandic-swap/web into feature/sas-login * Merge branch 'feature/sas-login' of bitbucket.org:scandic-swap/web into feature/sas-login * Add TooManyCodesError component * Refactor GenericError to support new errors * Add FailedAttemptsError * remove removed component <VWOScript/> * Merge branch 'feature/sas-login' of bitbucket.org:scandic-swap/web into feature/sas-login * remove local cookie-bot reference * Fix sas campaign logo scaling * feature toggle the SAS stuff * Merge branch 'feature/sas-login' of bitbucket.org:scandic-swap/web into feature/sas-login * fix: use env vars for SAS endpoints Approved-by: Linus Flood
129 lines
3.4 KiB
TypeScript
129 lines
3.4 KiB
TypeScript
import { cookies } from "next/headers"
|
||
import { redirect, RedirectType } 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 from "./OneTimePasswordForm"
|
||
|
||
import type { LangParams, PageArgs, SearchParams } from "@/types/params"
|
||
import type { Lang } from "@/constants/languages"
|
||
|
||
const searchParamsSchema = z.object({
|
||
intent: z.enum(["link"]),
|
||
to: z.string(),
|
||
error: z.enum(["invalidCode"]).optional(),
|
||
})
|
||
|
||
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 errors = {
|
||
invalidCode: intl.formatMessage({
|
||
id: "The code you’ve entered is incorrect.",
|
||
}),
|
||
}
|
||
|
||
async function handleOtpVerified({ otp }: { otp: string }) {
|
||
"use server"
|
||
|
||
const [data, error] = await safeTry(
|
||
serverClient().partner.sas.verifyOtp({ otp })
|
||
)
|
||
|
||
// TODO correct status?
|
||
// TODO handle all errors
|
||
// 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 (intent) {
|
||
case "link":
|
||
return handleLinkAccount({ lang: params.lang })
|
||
default:
|
||
throw new Error("")
|
||
}
|
||
}
|
||
|
||
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 ? errors[error] : undefined}
|
||
/>
|
||
)
|
||
}
|
||
|
||
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 (error) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
async function handleLinkAccount({ lang }: { lang: Lang }) {
|
||
const [res, error] = await safeTry(serverClient().partner.sas.linkAccount())
|
||
if (!res || error) {
|
||
console.error("[SAS] link account error", error)
|
||
redirect(`/${lang}/sas-x-scandic/error?errorCode=link_error`)
|
||
}
|
||
|
||
console.log("[SAS] link account response", res)
|
||
switch (res.linkingState) {
|
||
case "linked":
|
||
redirect(`/${lang}/sas-x-scandic/link/success`, RedirectType.replace)
|
||
break
|
||
}
|
||
}
|