119 lines
3.2 KiB
TypeScript
119 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" ||
|
|
stateResult.data.intent === "unlink"
|
|
) {
|
|
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`)
|
|
}
|