diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 09d3847716..e3d62c42c4 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -16,6 +16,7 @@ const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!; const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!; const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!; const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!; +const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!; const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com'; const POSTHOG_PROJECT_API_KEY = process.env.POSTHOG_PROJECT_API_KEY! || @@ -56,6 +57,7 @@ export { CLIENT_SECRET_HEROKU, CLIENT_SECRET_VERCEL, CLIENT_SECRET_NETLIFY, + CLIENT_SLUG_VERCEL, POSTHOG_HOST, POSTHOG_PROJECT_API_KEY, PRIVATE_KEY, diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 3cef519cc2..e6545a1021 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -61,6 +61,8 @@ const syncSecrets = async ({ }); break; } + + // TODO: set integration to inactive if it was not synced correctly (send alert?) } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -101,8 +103,8 @@ const syncSecretsHeroku = async ({ }); await axios.patch( - `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, - secrets, + `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, + secrets, { headers: { Accept: 'application/vnd.heroku+json; version=3', @@ -174,9 +176,9 @@ const syncSecretsVercel = async ({ [secret.key]: secret }), {}); - let updateSecrets: VercelSecret[] = []; - let deleteSecrets: VercelSecret[] = []; - let newSecrets: VercelSecret[] = []; + const updateSecrets: VercelSecret[] = []; + const deleteSecrets: VercelSecret[] = []; + const newSecrets: VercelSecret[] = []; // Identify secrets to create Object.keys(secrets).map((key) => { @@ -286,6 +288,173 @@ const syncSecretsNetlify = async ({ secrets: any; accessToken: string; }) => { + // TODO: Netlify revision in progress + // try { + // const getParams = new URLSearchParams({ + // context_name: integration.context, + // site_id: integration.siteId + // }); + + // const res = (await axios.get( + // `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + // { + // params: getParams, + // headers: { + // Authorization: `Bearer ${accessToken}` + // } + // } + // )) + // .data + // .reduce((obj: any, secret: any) => ({ + // ...obj, + // [secret.key]: secret + // }), {}); + + // res.forEach((r: any) => console.log(r)); + // console.log('getParams', getParams); + + // interface UpdateNetlifySecret { + // key: string; + // context: string; + // value: string; + // } + + // interface DeleteNetlifySecret { + // key: string; + // } + + // interface NewNetlifySecretValue { + // value: string; + // context: string; + // } + + // interface NewNetlifySecret { + // key: string; + // values: NewNetlifySecretValue[]; + // } + + // let updateSecrets: UpdateNetlifySecret[] = []; + // let deleteSecrets: DeleteNetlifySecret[] = []; + // let newSecrets: NewNetlifySecret[] = []; + + // interface NetlifyValue { + // id: string; + // value: string; + // context: string; + // role: string; + // } + + // // Identify secrets to create - GOOD + // Object.keys(secrets).map((key) => { + // if (!(key in res)) { + // // case: secret does not exist in Netlify -> create secret + // newSecrets.push({ + // key: key, + // values: [{ + // value: secrets[key], + // context: integration.context + // }] + // }); + // } else { + // // case: secret exists in Netlify + + // const netlifyContextsSet = new Set (res[key].values.map((netlifyValue: NetlifyValue) => netlifyValue.context)); + + // // TODO: check context/env. + // res[key].values.forEach((netlifyValue: NetlifyValue) => { + // if (netlifyValue.context === integration.context) { + // // case: Netlify value context matches integration context + // // TODO: check if value has changed + // } + // }); + // } + // }); + + // // Identify secrets to update and delete + // Object.keys(res).map((key) => { + // if (key in secrets) { + // // if (res[key] !== secrets[key]) { + // // // case: secret value has changed + // // updateSecrets.push({ + // // key: key, + // // context: integration.context, + // // value: secrets[key] + // // }); + // // } + + // // TODO: modify check and record of updated secrets + + // // case 1: new context added. + // } else { + // // case: secret has been deleted + // deleteSecrets.push({ + // key + // }); + // } + // }); + + // const syncParams = new URLSearchParams({ + // site_id: integration.siteId + // }); + + // console.log('Netlify newSecrets', newSecrets); + // newSecrets.forEach(secret => console.log(secret.values)); + // // Sync/push new secrets + // if (newSecrets.length > 0) { + // await axios.post( + // `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + // newSecrets, + // { + // params: syncParams, + // headers: { + // Authorization: `Bearer ${accessToken}` + // } + // } + // ); + // } + + // console.log('Netlify updateSecrets', updateSecrets); + // // Sync/push updated secrets + // if (updateSecrets.length > 0) { + + // updateSecrets.forEach(async (secret: UpdateNetlifySecret) => { + // await axios.patch( + // `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, + // { + // context: secret.context, + // value: secret.value + // }, + // { + // params: syncParams, + // headers: { + // Authorization: `Bearer ${accessToken}` + // } + // } + // ); + // }); + // } + + // console.log('Netlify deleteSecrets', deleteSecrets); + // // Delete secrets + // if (deleteSecrets.length > 0) { + // deleteSecrets.forEach(async (secret: DeleteNetlifySecret) => { + // await axios.delete( + // `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, + // { + // params: syncParams, + // headers: { + // Authorization: `Bearer ${accessToken}` + // } + // } + // ); + // }); + // } + // } catch (err) { + // Sentry.setUser(null); + // Sentry.captureException(err); + // throw new Error('Failed to sync secrets to Heroku'); + // } + try { const getParams = new URLSearchParams({ context_name: integration.context, @@ -327,9 +496,9 @@ const syncSecretsNetlify = async ({ values: NewNetlifySecretValue[]; } - let updateSecrets: UpdateNetlifySecret[] = []; - let deleteSecrets: DeleteNetlifySecret[] = []; - let newSecrets: NewNetlifySecret[] = []; + const updateSecrets: UpdateNetlifySecret[] = []; + const deleteSecrets: DeleteNetlifySecret[] = []; + const newSecrets: NewNetlifySecret[] = []; // Identify secrets to create Object.keys(secrets).map((key) => { diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index c02a2389fb..35c6323894 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -1,6 +1,7 @@ import { CLIENT_ID_HEROKU, - CLIENT_ID_NETLIFY + CLIENT_ID_NETLIFY, + CLIENT_SLUG_VERCEL } from '../config'; // integrations @@ -43,13 +44,14 @@ const INTEGRATION_OPTIONS = [ isAvailable: true, type: 'vercel', clientId: '', + clientSlug: CLIENT_SLUG_VERCEL, docsLink: '' }, { name: 'Netlify', slug: 'netlify', image: 'Netlify', - isAvailable: true, + isAvailable: false, type: 'oauth2', clientId: CLIENT_ID_NETLIFY, docsLink: '' diff --git a/frontend/components/basic/Listbox.tsx b/frontend/components/basic/Listbox.tsx index 2ff0179dd6..e726b7f56c 100644 --- a/frontend/components/basic/Listbox.tsx +++ b/frontend/components/basic/Listbox.tsx @@ -11,7 +11,7 @@ import { Listbox, Transition } from "@headlessui/react"; interface ListBoxProps { selected: string; onChange: (arg: string) => void; - data: string[]; + data: string[] | null; text?: string; buttonAction?: () => void; isFull?: boolean; diff --git a/frontend/components/integrations/Integration.tsx b/frontend/components/integrations/Integration.tsx index de0747409c..053269be40 100644 --- a/frontend/components/integrations/Integration.tsx +++ b/frontend/components/integrations/Integration.tsx @@ -14,9 +14,11 @@ import deleteIntegration from "../../pages/api/integrations/DeleteIntegration" import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps"; import updateIntegration from "../../pages/api/integrations/updateIntegration" import { + contextNetlifyMapping, envMapping, reverseContextNetlifyMapping, - reverseEnvMapping} from "../../public/data/frequentConstants"; + reverseEnvMapping, +} from "../../public/data/frequentConstants"; interface Integration { _id: string; @@ -25,6 +27,7 @@ interface Integration { integration: string; integrationAuth: string; isActive: boolean; + context: string; } interface IntegrationApp { @@ -69,7 +72,7 @@ const Integration = ({ setIntegrationTarget("Development"); break; case "netlify": - setIntegrationContext("All"); + setIntegrationContext(integration?.context ? contextNetlifyMapping[integration.context] : "Local development"); break; default: break; @@ -93,7 +96,7 @@ const Integration = ({ "Production", "Preview", "Development" - ] : []} + ] : null} selected={"Production"} onChange={setIntegrationTarget} /> @@ -107,12 +110,11 @@ const Integration = ({ @@ -138,7 +140,7 @@ const Integration = ({ "Staging", "Testing", "Production", - ] : []} + ] : null} selected={integrationEnvironment} onChange={(environment) => { setIntegrationEnvironment(environment); @@ -166,7 +168,7 @@ const Integration = ({ APP app.name) : []} + data={!integration.isActive ? apps.map((app) => app.name) : null} selected={integrationApp} onChange={(app) => { setIntegrationApp(app); @@ -190,7 +192,8 @@ const Integration = ({ onButtonPressed={async () => { const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined - const siteId = siteApp ? siteApp.siteId : null; + const siteId = siteApp?.siteId ? siteApp.siteId : null; + const result = await updateIntegration({ integrationId: integration._id, environment: envMapping[integrationEnvironment], @@ -200,6 +203,7 @@ const Integration = ({ context: integrationContext ? reverseContextNetlifyMapping[integrationContext] : null, siteId }); + router.reload(); }} color="mineshaft" diff --git a/frontend/pages/integrations/[id].js b/frontend/pages/integrations/[id].js index be0aff9213..a7b5f6b133 100644 --- a/frontend/pages/integrations/[id].js +++ b/frontend/pages/integrations/[id].js @@ -41,7 +41,7 @@ export default function Integrations() { setCloudIntegrationOptions( await getIntegrationOptions() ); - + // get project integration authorizations setIntegrationAuths( await getWorkspaceAuthorizations({ @@ -134,7 +134,7 @@ export default function Integrations() { window.location = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`; break; case 'Vercel': - window.location = `https://vercel.com/integrations/infisical-dev/new?state=${state}`; + window.location = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`; break; case 'Netlify': window.location = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`; diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index a447225d49..d2beee6d26 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -16,8 +16,14 @@ const reverseEnvMapping: Mapping = { test: "Testing", }; +const contextNetlifyMapping: Mapping = { + "dev": "Local development", + "branch-deploy": "Branch deploys", + "deploy-review": "Deploy Previews", + "production": "Production" +} + const reverseContextNetlifyMapping: Mapping = { - "All": "all", "Local development": "dev", "Branch deploys": "branch-deploy", "Deploy Previews": "deploy-preview", @@ -25,6 +31,7 @@ const reverseContextNetlifyMapping: Mapping = { } export { + contextNetlifyMapping, envMapping, reverseContextNetlifyMapping, - reverseEnvMapping}; + reverseEnvMapping}