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`) }