feat(integrations/vercel): custom environments support

This commit is contained in:
Daniel Hougaard
2025-01-27 23:08:47 +01:00
parent a93bfa69c9
commit 10c10642a1
10 changed files with 255 additions and 24 deletions
@@ -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",
@@ -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;
@@ -170,10 +180,19 @@ const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null;
}
});
for (const project of data.projects) {
console.log(project.customEnvironments);
}
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
})) ?? []
});
});
@@ -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
};
};
@@ -284,3 +284,8 @@ export type TOctopusDeployVariableSet = {
Self: string;
};
};
export type GetVercelCustomEnvironmentsDTO = {
teamId: string;
id: string;
} & Omit<TProjectPermission, "projectId">;
@@ -1450,9 +1450,13 @@ const syncSecretsVercel = async ({
secrets: Record<string, { value: string; comment?: string } | null>;
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
@@ -5,6 +5,7 @@ import { request } from "@app/lib/config/request";
import { BadRequestError, ForbiddenRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { Integrations, IntegrationUrls } from "./integration-list";
import { AxiosError } from "axios";
type ExchangeCodeAzureResponse = {
token_type: string;
@@ -197,15 +198,22 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
}
const res = (
await request.post<ExchangeCodeVercelResponse>(
IntegrationUrls.VERCEL_TOKEN_URL,
new URLSearchParams({
code,
client_id: appCfg.CLIENT_ID_VERCEL,
client_secret: appCfg.CLIENT_SECRET_VERCEL,
redirect_uri: `${appCfg.SITE_URL}/integrations/vercel/oauth2/callback`
await request
.post<ExchangeCodeVercelResponse>(
IntegrationUrls.VERCEL_TOKEN_URL,
new URLSearchParams({
code,
client_id: appCfg.CLIENT_ID_VERCEL,
client_secret: appCfg.CLIENT_SECRET_VERCEL,
redirect_uri: `${appCfg.SITE_URL}/integrations/vercel/oauth2/callback`
})
)
.catch((e) => {
if (e instanceof AxiosError) {
console.log(e.response?.data);
}
throw e;
})
)
).data;
return {