chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { redirect } from "@remix-run/server-runtime";
|
||||
import { fromPromise } from "neverthrow";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { VercelIntegrationRepository, type TokenResponse } from "~/models/vercelIntegration.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { requestUrl } from "~/utils/requestUrl.server";
|
||||
import { v3ProjectSettingsIntegrationsPath } from "~/utils/pathBuilder";
|
||||
import { validateVercelOAuthState } from "~/v3/vercel/vercelOAuthState.server";
|
||||
|
||||
const VercelConnectSchema = z.object({
|
||||
state: z.string(),
|
||||
configurationId: z.string().optional(),
|
||||
code: z.string(),
|
||||
next: z.string().optional(),
|
||||
origin: z.enum(["marketplace", "dashboard"]),
|
||||
});
|
||||
|
||||
async function createOrFindVercelIntegration(
|
||||
organizationId: string,
|
||||
projectId: string,
|
||||
tokenResponse: TokenResponse,
|
||||
configurationId: string | undefined,
|
||||
origin: "marketplace" | "dashboard"
|
||||
): Promise<void> {
|
||||
const project = await prisma.project.findUnique({
|
||||
where: { id: projectId },
|
||||
include: { organization: true },
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new Error("Project not found");
|
||||
}
|
||||
|
||||
let orgIntegration = await VercelIntegrationRepository.findVercelOrgIntegrationByTeamId(
|
||||
organizationId,
|
||||
tokenResponse.teamId ?? null
|
||||
);
|
||||
|
||||
if (orgIntegration) {
|
||||
await VercelIntegrationRepository.updateVercelOrgIntegrationToken({
|
||||
integrationId: orgIntegration.id,
|
||||
accessToken: tokenResponse.accessToken,
|
||||
tokenType: tokenResponse.tokenType,
|
||||
teamId: tokenResponse.teamId ?? null,
|
||||
userId: tokenResponse.userId,
|
||||
installationId: configurationId,
|
||||
raw: tokenResponse.raw,
|
||||
});
|
||||
} else {
|
||||
await VercelIntegrationRepository.createVercelOrgIntegration({
|
||||
accessToken: tokenResponse.accessToken,
|
||||
tokenType: tokenResponse.tokenType,
|
||||
teamId: tokenResponse.teamId ?? null,
|
||||
userId: tokenResponse.userId,
|
||||
installationId: configurationId,
|
||||
organization: project.organization,
|
||||
raw: tokenResponse.raw,
|
||||
origin,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
const url = requestUrl(request);
|
||||
|
||||
const parsed = VercelConnectSchema.safeParse(Object.fromEntries(url.searchParams));
|
||||
if (!parsed.success) {
|
||||
logger.warn("Invalid Vercel connect params", { error: parsed.error });
|
||||
throw new Response("Invalid parameters", { status: 400 });
|
||||
}
|
||||
|
||||
const { state, configurationId, code, next, origin } = parsed.data;
|
||||
|
||||
const validationResult = await validateVercelOAuthState(state);
|
||||
if (!validationResult.ok) {
|
||||
logger.warn("Invalid Vercel OAuth state JWT", { error: validationResult.error });
|
||||
|
||||
if (
|
||||
validationResult.error?.includes("expired") ||
|
||||
validationResult.error?.includes("Token has expired")
|
||||
) {
|
||||
const params = new URLSearchParams({ error: "expired" });
|
||||
return redirect(`/vercel/onboarding?${params.toString()}`);
|
||||
}
|
||||
|
||||
throw new Response("Invalid state", { status: 400 });
|
||||
}
|
||||
|
||||
const stateData = validationResult.state;
|
||||
|
||||
const project = await prisma.project.findFirst({
|
||||
where: {
|
||||
id: stateData.projectId,
|
||||
organizationId: stateData.organizationId,
|
||||
deletedAt: null,
|
||||
organization: {
|
||||
members: {
|
||||
some: { userId },
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
logger.warn("Project not found or access denied", {
|
||||
projectId: stateData.projectId,
|
||||
userId,
|
||||
});
|
||||
throw new Response("Project not found", { status: 404 });
|
||||
}
|
||||
|
||||
const tokenResult = await VercelIntegrationRepository.exchangeCodeForToken(code);
|
||||
if (tokenResult.isErr()) {
|
||||
const params = new URLSearchParams({ error: "expired" });
|
||||
return redirect(`/vercel/onboarding?${params.toString()}`);
|
||||
}
|
||||
const tokenResponse = tokenResult.value;
|
||||
|
||||
const environment = await prisma.runtimeEnvironment.findFirst({
|
||||
where: {
|
||||
projectId: project.id,
|
||||
slug: stateData.environmentSlug,
|
||||
archivedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!environment) {
|
||||
logger.warn("Environment not found", {
|
||||
projectId: project.id,
|
||||
environmentSlug: stateData.environmentSlug,
|
||||
});
|
||||
throw new Response("Environment not found", { status: 404 });
|
||||
}
|
||||
|
||||
const settingsPath = v3ProjectSettingsIntegrationsPath(
|
||||
{ slug: stateData.organizationSlug },
|
||||
{ slug: stateData.projectSlug },
|
||||
{ slug: environment.slug }
|
||||
);
|
||||
|
||||
const result = await fromPromise(
|
||||
createOrFindVercelIntegration(
|
||||
stateData.organizationId,
|
||||
stateData.projectId,
|
||||
tokenResponse,
|
||||
configurationId,
|
||||
origin
|
||||
),
|
||||
(error) => error
|
||||
);
|
||||
|
||||
if (result.isErr()) {
|
||||
logger.error("Failed to complete Vercel integration", { error: result.error });
|
||||
throw redirect(settingsPath);
|
||||
}
|
||||
|
||||
logger.info("Vercel organization integration created successfully", {
|
||||
organizationId: stateData.organizationId,
|
||||
projectId: stateData.projectId,
|
||||
teamId: tokenResponse.teamId,
|
||||
});
|
||||
|
||||
const params = new URLSearchParams({ vercelOnboarding: "true", origin });
|
||||
if (next) {
|
||||
params.set("next", next);
|
||||
}
|
||||
|
||||
return redirect(`${settingsPath}?${params.toString()}`);
|
||||
}
|
||||
Reference in New Issue
Block a user