Files
web/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/callback/route.ts
Anton Gunnarsson 18288cb849 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
2025-02-07 14:18:00 +00:00

116 lines
3.2 KiB
TypeScript

import { cookies } from "next/headers"
import { redirect } from "next/navigation"
import { z } from "zod"
import { env } from "@/env/server"
import { serverClient } from "@/lib/trpc/server"
import { safeTry } from "@/utils/safeTry"
import { SAS_TOKEN_STORAGE_KEY, stateSchema } from "../sasUtils"
import type { NextRequest } from "next/server"
const searchParamsSchema = z.object({
code: z.string(),
state: z.string(),
})
const tokenResponseSchema = z.object({
access_token: z.string(),
expires_in: z.number(),
token_type: z.literal("Bearer"),
})
export async function GET(
request: NextRequest,
{ params }: { params: { lang: string } }
) {
const { lang } = params
const result = searchParamsSchema.safeParse({
code: request.nextUrl.searchParams.get("code"),
state: request.nextUrl.searchParams.get("state"),
})
if (!result.success) {
console.error("[SAS] Invalid search params", result.error)
redirect(`/${lang}/sas-x-scandic/error?errorCode=invalid_query`)
}
const { code, state } = result.data
const tokenResponse = await fetch(
new URL("oauth/token", env.SAS_AUTH_ENDPOINT),
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "authorization_code",
code: code,
redirect_uri: new URL(
`/${lang}/sas-x-scandic/callback`,
new URL(env.PUBLIC_URL)
).toString(),
client_id: env.SAS_AUTH_CLIENTID,
}),
}
)
if (!tokenResponse.ok) {
const error = await tokenResponse.text()
console.error("[SAS] Failed to get token", error)
redirect(`/${lang}/sas-x-scandic/error?errorCode=token_error`)
}
const tokenData = tokenResponseSchema.parse(await tokenResponse.json())
const stateResult = stateSchema.safeParse(
JSON.parse(decodeURIComponent(state))
)
if (!stateResult.success) {
redirect(`/${lang}/sas-x-scandic/error?errorCode=invalid_state`)
}
const cookieStore = cookies()
cookieStore.set(SAS_TOKEN_STORAGE_KEY, tokenData.access_token, {
maxAge: 3600,
httpOnly: true,
})
if (stateResult.data.intent === "link") {
const [data, error] = await safeTry(
serverClient().partner.sas.requestOtp({})
)
if (!data || error) {
console.error("[SAS] Failed to request OTP", 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)
const otpUrl = new URL(
`/${lang}/sas-x-scandic/otp`,
new URL(env.PUBLIC_URL)
)
otpUrl.searchParams.set("intent", stateResult.data.intent)
otpUrl.searchParams.set("to", data.otpReceiver)
redirect(otpUrl.toString())
}
redirect(`/${lang}/sas-x-scandic/error?errorCode=unknown_intent`)
}