chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { redirect } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { getUserId } from "~/services/session.server";
|
||||
import { setReferralSourceCookie } from "~/services/referralSource.server";
|
||||
import { requestUrl } from "~/utils/requestUrl.server";
|
||||
import { sanitizeVercelNextUrl } from "~/v3/vercel/vercelUrls.server";
|
||||
|
||||
const VercelCallbackSchema = z
|
||||
.object({
|
||||
code: z.string().optional(),
|
||||
state: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
error_description: z.string().optional(),
|
||||
configurationId: z.string().optional(),
|
||||
next: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
if (request.method.toUpperCase() !== "GET") {
|
||||
throw new Response("Method Not Allowed", { status: 405 });
|
||||
}
|
||||
|
||||
const userId = await getUserId(request);
|
||||
if (!userId) {
|
||||
const currentUrl = new URL(request.url);
|
||||
const redirectTo = `${currentUrl.pathname}${currentUrl.search}`;
|
||||
const referralCookie = await setReferralSourceCookie("vercel");
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append("Set-Cookie", referralCookie);
|
||||
|
||||
throw redirect(`/login?redirectTo=${encodeURIComponent(redirectTo)}`, { headers });
|
||||
}
|
||||
|
||||
const url = requestUrl(request);
|
||||
const parsed = VercelCallbackSchema.safeParse(Object.fromEntries(url.searchParams));
|
||||
|
||||
if (!parsed.success) {
|
||||
logger.error("Invalid Vercel callback params", { error: parsed.error });
|
||||
throw new Response("Invalid callback parameters", { status: 400 });
|
||||
}
|
||||
|
||||
const { code, state, error, error_description, configurationId, next: rawNextUrl } = parsed.data;
|
||||
|
||||
// Sanitize the `next` parameter to prevent open redirects
|
||||
const nextUrl = sanitizeVercelNextUrl(rawNextUrl);
|
||||
|
||||
if (error) {
|
||||
logger.error("Vercel OAuth error", { error, error_description });
|
||||
throw new Response("Vercel OAuth error", { status: 500 });
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
logger.error("Missing authorization code from Vercel callback");
|
||||
throw new Response("Missing authorization code", { status: 400 });
|
||||
}
|
||||
|
||||
// Route with state: dashboard-invoked flow
|
||||
if (state) {
|
||||
const params = new URLSearchParams({ state, code, origin: "dashboard" });
|
||||
if (configurationId) params.set("configurationId", configurationId);
|
||||
if (nextUrl) params.set("next", nextUrl);
|
||||
return redirect(`/vercel/connect?${params.toString()}`);
|
||||
}
|
||||
|
||||
// Route without state but with configurationId: marketplace-invoked flow
|
||||
if (configurationId) {
|
||||
const params = new URLSearchParams({ code, configurationId, origin: "marketplace" });
|
||||
if (nextUrl) params.set("next", nextUrl);
|
||||
return redirect(`/vercel/onboarding?${params.toString()}`);
|
||||
}
|
||||
|
||||
logger.error("Missing both state and configurationId from Vercel callback");
|
||||
throw new Response("Missing state or configurationId parameter", { status: 400 });
|
||||
}
|
||||
Reference in New Issue
Block a user