import { redirect } from "@remix-run/server-runtime"; import { z } from "zod"; import { $replica } from "~/db.server"; import { dashboardLoader } from "~/services/routeBuilders/dashboardBuilder"; import { logger } from "~/services/logger.server"; import { resolveOrgIdFromSlug } from "~/models/organization.server"; import { OrgIntegrationRepository } from "~/models/orgIntegration.server"; import { generateVercelOAuthState } from "~/v3/vercel/vercelOAuthState.server"; import { findProjectBySlug } from "~/models/project.server"; const QuerySchema = z.object({ org_slug: z.string(), project_slug: z.string(), }); export const loader = dashboardLoader( { // The org for the auth scope comes from the `org_slug` query param. context: async (_params, request) => { const orgSlug = new URL(request.url).searchParams.get("org_slug"); if (!orgSlug) return {}; const organizationId = await resolveOrgIdFromSlug(orgSlug); return organizationId ? { organizationId } : {}; }, authorization: { action: "write", resource: { type: "vercel" } }, // Redirect endpoint (no UI): keep redirecting on denial rather than // throwing the permission panel. unauthorizedRedirect: "/", }, async ({ request, user }) => { const searchParams = new URL(request.url).searchParams; const parsed = QuerySchema.safeParse(Object.fromEntries(searchParams)); if (!parsed.success) { logger.warn("Vercel App installation redirect with invalid params", { searchParams, error: parsed.error, }); throw redirect("/"); } const { org_slug, project_slug } = parsed.data; // Find the organization const org = await $replica.organization.findFirst({ where: { slug: org_slug, members: { some: { userId: user.id } }, deletedAt: null }, orderBy: { createdAt: "desc" }, select: { id: true, }, }); if (!org) { throw redirect("/"); } // Find the project const project = await findProjectBySlug(org_slug, project_slug, user.id); if (!project) { logger.warn("Vercel App installation attempt for non-existent project", { org_slug, project_slug, userId: user.id, }); throw redirect("/"); } // Use "prod" as the default environment slug for the redirect // The callback will redirect to the settings page for this environment const environmentSlug = "prod"; // Generate JWT state token const stateToken = await generateVercelOAuthState({ organizationId: org.id, projectId: project.id, environmentSlug, organizationSlug: org_slug, projectSlug: project_slug, }); // Generate Vercel install URL const vercelInstallUrl = OrgIntegrationRepository.vercelInstallUrl(stateToken); return redirect(vercelInstallUrl); } );