diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index ffeb748b8..185738de6 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -1151,6 +1151,50 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) } }); + server.route({ + method: "GET", + url: "/:integrationAuthId/vercel/custom-environments", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + querystring: z.object({ + teamId: z.string().trim() + }), + params: z.object({ + integrationAuthId: z.string().trim() + }), + response: { + 200: z.object({ + environments: z + .object({ + appId: z.string(), + customEnvironments: z + .object({ + id: z.string(), + slug: z.string() + }) + .array() + }) + .array() + }) + } + }, + handler: async (req) => { + const environments = await server.services.integrationAuth.getVercelCustomEnvironments({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId, + teamId: req.query.teamId + }); + + return { environments }; + } + }); + server.route({ method: "GET", url: "/:integrationAuthId/octopus-deploy/spaces", diff --git a/backend/src/services/integration-auth/integration-app-list.ts b/backend/src/services/integration-auth/integration-app-list.ts index 3b8078cc1..126037895 100644 --- a/backend/src/services/integration-auth/integration-app-list.ts +++ b/backend/src/services/integration-auth/integration-app-list.ts @@ -132,16 +132,26 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { /** * Return list of names of apps for Vercel integration + * This is re-used for getting custom environments for Vercel */ -const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => { - const apps: Array<{ name: string; appId: string }> = []; +export const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => { + const apps: Array<{ name: string; appId: string; customEnvironments: Array<{ slug: string; id: string }> }> = []; const limit = "20"; let hasMorePages = true; let next: number | null = null; interface Response { - projects: { name: string; id: string }[]; + projects: { + name: string; + id: string; + customEnvironments?: { + id: string; + type: string; + description: string; + slug: string; + }[]; + }[]; pagination: { count: number; next: number | null; @@ -173,7 +183,12 @@ const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; data.projects.forEach((a) => { apps.push({ name: a.name, - appId: a.id + appId: a.id, + customEnvironments: + a.customEnvironments?.map((env) => ({ + slug: env.slug, + id: env.id + })) ?? [] }); }); diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index d96643a19..61f753348 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -25,11 +25,12 @@ import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; -import { getApps } from "./integration-app-list"; +import { getApps, getAppsVercel } from "./integration-app-list"; import { TCircleCIContext } from "./integration-app-types"; import { TIntegrationAuthDALFactory } from "./integration-auth-dal"; import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema"; import { + GetVercelCustomEnvironmentsDTO, OctopusDeployScope, TBitbucketEnvironment, TBitbucketWorkspace, @@ -1825,6 +1826,41 @@ export const integrationAuthServiceFactory = ({ return integrationAuthDAL.create(newIntegrationAuth); }; + const getVercelCustomEnvironments = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + teamId, + id + }: GetVercelCustomEnvironmentsDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: integrationAuth.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + + const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); + + const vercelApps = await getAppsVercel({ + accessToken, + teamId + }); + + return vercelApps.map((app) => ({ + customEnvironments: app.customEnvironments, + appId: app.appId + })); + }; + const getOctopusDeploySpaces = async ({ actorId, actor, @@ -1944,6 +1980,7 @@ export const integrationAuthServiceFactory = ({ getIntegrationAccessToken, duplicateIntegrationAuth, getOctopusDeploySpaces, - getOctopusDeployScopeValues + getOctopusDeployScopeValues, + getVercelCustomEnvironments }; }; diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts index 68d7bf5b9..8efe6b851 100644 --- a/backend/src/services/integration-auth/integration-auth-types.ts +++ b/backend/src/services/integration-auth/integration-auth-types.ts @@ -284,3 +284,8 @@ export type TOctopusDeployVariableSet = { Self: string; }; }; + +export type GetVercelCustomEnvironmentsDTO = { + teamId: string; + id: string; +} & Omit; diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 96e8ec598..249307f8d 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -1450,9 +1450,13 @@ const syncSecretsVercel = async ({ secrets: Record; accessToken: string; }) => { + const isCustomEnvironment = !["development", "preview", "production"].includes( + integration.targetEnvironment as string + ); interface VercelSecret { id?: string; type: string; + customEnvironmentIds?: string[]; key: string; value: string; target: string[]; @@ -1486,6 +1490,16 @@ const syncSecretsVercel = async ({ } ) ).data.envs.filter((secret) => { + if (isCustomEnvironment) { + if (!secret.customEnvironmentIds?.includes(integration.targetEnvironment as string)) { + // case: secret does not have the same custom environment + return false; + } + + // no need to check for preview environment, as custom environments are not available in preview + return true; + } + if (!secret.target.includes(integration.targetEnvironment as string)) { // case: secret does not have the same target environment return false; @@ -1583,7 +1597,13 @@ const syncSecretsVercel = async ({ key, value: infisicalSecrets[key]?.value, type: "encrypted", - target: [integration.targetEnvironment as string], + ...(isCustomEnvironment + ? { + customEnvironmentIds: [integration.targetEnvironment as string] + } + : { + target: [integration.targetEnvironment as string] + }), ...(integration.path ? { gitBranch: integration.path @@ -1607,9 +1627,19 @@ const syncSecretsVercel = async ({ key, value: infisicalSecrets[key]?.value, type: res[key].type, - target: res[key].target.includes(integration.targetEnvironment as string) - ? [...res[key].target] - : [...res[key].target, integration.targetEnvironment as string], + + ...(!isCustomEnvironment + ? { + target: res[key].target.includes(integration.targetEnvironment as string) + ? [...res[key].target] + : [...res[key].target, integration.targetEnvironment as string] + } + : { + customEnvironmentIds: res[key].customEnvironmentIds?.includes(integration.targetEnvironment as string) + ? [...(res[key].customEnvironmentIds || [])] + : [...(res[key]?.customEnvironmentIds || []), integration.targetEnvironment as string] + }), + ...(integration.path ? { gitBranch: integration.path diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index e7ee5928a..eab946ba6 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -16,5 +16,6 @@ export { useGetIntegrationAuthTeamCityBuildConfigs, useGetIntegrationAuthTeams, useGetIntegrationAuthVercelBranches, + useGetIntegrationAuthVercelCustomEnvironments, useSaveIntegrationAccessToken } from "./queries"; diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 44b199a24..f62887043 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -21,7 +21,8 @@ import { Team, TeamCityBuildConfig, TGetIntegrationAuthOctopusDeployScopeValuesDTO, - TOctopusDeployVariableSetScopeValues + TOctopusDeployVariableSetScopeValues, + VercelEnvironment } from "./types"; const integrationAuthKeys = { @@ -132,7 +133,9 @@ const integrationAuthKeys = { }: TGetIntegrationAuthOctopusDeployScopeValuesDTO) => [{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const, getIntegrationAuthCircleCIOrganizations: (integrationAuthId: string) => - [{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const + [{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const, + getIntegrationAuthVercelCustomEnv: (integrationAuthId: string, teamId: string) => + [{ integrationAuthId, teamId }, "integrationAuthVercelCustomEnv"] as const }; const fetchIntegrationAuthById = async (integrationAuthId: string) => { @@ -362,6 +365,29 @@ const fetchIntegrationAuthQoveryScopes = async ({ return undefined; }; +const fetchIntegrationAuthVercelCustomEnvironments = async ({ + integrationAuthId, + teamId +}: { + integrationAuthId: string; + teamId: string; +}) => { + const { + data: { environments } + } = await apiRequest.get<{ + environments: { + appId: string; + customEnvironments: VercelEnvironment[]; + }[]; + }>(`/api/v1/integration-auth/${integrationAuthId}/vercel/custom-environments`, { + params: { + teamId + } + }); + + return environments; +}; + const fetchIntegrationAuthHerokuPipelines = async ({ integrationAuthId }: { @@ -730,6 +756,24 @@ export const useGetIntegrationAuthQoveryScopes = ({ }); }; +export const useGetIntegrationAuthVercelCustomEnvironments = ({ + integrationAuthId, + teamId +}: { + integrationAuthId: string; + teamId: string; +}) => { + return useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthVercelCustomEnv(integrationAuthId, teamId), + queryFn: () => + fetchIntegrationAuthVercelCustomEnvironments({ + integrationAuthId, + teamId + }), + enabled: Boolean(teamId && integrationAuthId) + }); +}; + export const useGetIntegrationAuthHerokuPipelines = ({ integrationAuthId }: { diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index e2dee6067..b57e5aeca 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -43,6 +43,11 @@ export type Environment = { environmentId: string; }; +export type VercelEnvironment = { + id: string; + slug: string; +}; + export type ChecklyGroup = { name: string; groupId: number; diff --git a/frontend/src/pages/secret-manager/integrations/VercelConfigurePage/VercelConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/VercelConfigurePage/VercelConfigurePage.tsx index a113dba15..fabccb6bb 100644 --- a/frontend/src/pages/secret-manager/integrations/VercelConfigurePage/VercelConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/VercelConfigurePage/VercelConfigurePage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Helmet } from "react-helmet"; import { faArrowUpRightFromSquare, @@ -25,7 +25,8 @@ import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthApps, useGetIntegrationAuthById, - useGetIntegrationAuthVercelBranches + useGetIntegrationAuthVercelBranches, + useGetIntegrationAuthVercelCustomEnvironments } from "@app/hooks/api/integrationAuth"; import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types"; @@ -75,6 +76,11 @@ export const VercelConfigurePage = () => { teamId: integrationAuth?.teamId as string }); + const { data: customEnvironments } = useGetIntegrationAuthVercelCustomEnvironments({ + teamId: integrationAuth?.teamId as string, + integrationAuthId: integrationAuthId as string + }); + const { data: branches } = useGetIntegrationAuthVercelBranches({ integrationAuthId: integrationAuthId as string, appId: targetAppId @@ -135,6 +141,26 @@ export const VercelConfigurePage = () => { } }; + const selectedVercelEnvironments = useMemo(() => { + let selectedEnvironments = vercelEnvironments; + + const environments = customEnvironments?.find( + (e) => e.appId === targetAppId + )?.customEnvironments; + + if (environments && environments.length > 0) { + selectedEnvironments = [ + ...selectedEnvironments, + ...environments.map((env) => ({ + name: env.slug, + slug: env.id + })) + ]; + } + + return selectedEnvironments; + }, [targetAppId, customEnvironments]); + return integrationAuth && selectedSourceEnvironment && integrationAuthApps && @@ -210,7 +236,13 @@ export const VercelConfigurePage = () => { >