diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 5575ceb44a..83d2bacdcc 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -11,7 +11,8 @@ const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!; const MONGO_URL = process.env.MONGO_URL!; const NODE_ENV = process.env.NODE_ENV! || 'production'; const OAUTH_CLIENT_SECRET_HEROKU = process.env.OAUTH_CLIENT_SECRET_HEROKU!; -const OAUTH_TOKEN_URL_HEROKU = process.env.OAUTH_TOKEN_URL_HEROKU!; +const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!; +const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!; const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com'; const POSTHOG_PROJECT_API_KEY = process.env.POSTHOG_PROJECT_API_KEY! || @@ -47,7 +48,8 @@ export { MONGO_URL, NODE_ENV, OAUTH_CLIENT_SECRET_HEROKU, - OAUTH_TOKEN_URL_HEROKU, + CLIENT_SECRET_VERCEL, + CLIENT_ID_VERCEL, POSTHOG_HOST, POSTHOG_PROJECT_API_KEY, PRIVATE_KEY, diff --git a/backend/src/controllers/integrationAuthController.ts b/backend/src/controllers/integrationAuthController.ts index 829abc7c24..f38516e30a 100644 --- a/backend/src/controllers/integrationAuthController.ts +++ b/backend/src/controllers/integrationAuthController.ts @@ -4,7 +4,6 @@ import axios from 'axios'; import { readFileSync } from 'fs'; import { IntegrationAuth, Integration } from '../models'; import { INTEGRATION_SET, ENV_DEV } from '../variables'; -import { OAUTH_CLIENT_SECRET_HEROKU, OAUTH_TOKEN_URL_HEROKU } from '../config'; import { IntegrationService } from '../services'; import { getApps } from '../integrations'; diff --git a/backend/src/controllers/integrationController.ts b/backend/src/controllers/integrationController.ts index bf218f4579..87ee77dbe6 100644 --- a/backend/src/controllers/integrationController.ts +++ b/backend/src/controllers/integrationController.ts @@ -31,17 +31,21 @@ interface PushSecret { export const updateIntegration = async (req: Request, res: Response) => { let integration; + // TODO: add integration-specific validation to ensure that each + // integration has the correct fields populated in [Integration] + try { - const { app, environment, isActive } = req.body; + const { app, environment, isActive, target } = req.body; integration = await Integration.findOneAndUpdate( { _id: req.integration._id }, { - app, environment, - isActive + isActive, + app, + target }, { new: true @@ -49,7 +53,6 @@ export const updateIntegration = async (req: Request, res: Response) => { ); if (integration) { - // trigger event - push secrets EventService.handleEvent({ event: eventPushSecrets({ diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index d0e775bfc9..0c4bd49491 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -10,9 +10,16 @@ import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; import { BotService, IntegrationService } from '../services'; import { ENV_DEV, - EVENT_PUSH_SECRETS + EVENT_PUSH_SECRETS, + INTEGRATION_VERCEL } from '../variables'; +interface Update { + workspace: string; + integration: string; + teamId?: string; +} + /** * Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration * named [integration] @@ -49,29 +56,45 @@ const handleOAuthExchangeHelper = async ({ code }); - integrationAuth = await IntegrationAuth.findOneAndUpdate({ + // TODO: continue ironing out Vercel integration + + let update: Update = { workspace: workspaceId, integration - }, { + } + + switch (integration) { + case INTEGRATION_VERCEL: + update.teamId = res.teamId; + break; + } + + integrationAuth = await IntegrationAuth.findOneAndUpdate({ workspace: workspaceId, integration - }, { + }, update, { new: true, upsert: true }); - // set integration auth refresh token - await setIntegrationAuthRefreshHelper({ - integrationAuthId: integrationAuth._id.toString(), - refreshToken: res.refreshToken - }); + if (res.refreshToken) { + // case: refresh token returned from exchange + // set integration auth refresh token + await setIntegrationAuthRefreshHelper({ + integrationAuthId: integrationAuth._id.toString(), + refreshToken: res.refreshToken + }); + } - // set integration auth access token - await setIntegrationAuthAccessHelper({ - integrationAuthId: integrationAuth._id.toString(), - accessToken: res.accessToken, - accessExpiresAt: res.accessExpiresAt - }); + if (res.accessToken) { + // case: access token returned from exchange + // set integration auth access token + await setIntegrationAuthAccessHelper({ + integrationAuthId: integrationAuth._id.toString(), + accessToken: res.accessToken, + accessExpiresAt: res.accessExpiresAt + }); + } // initialize new integration after exchange await new Integration({ @@ -82,7 +105,6 @@ const handleOAuthExchangeHelper = async ({ integration, integrationAuth: integrationAuth._id }).save(); - } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -104,7 +126,7 @@ const syncIntegrationsHelper = async ({ try { integrations = await Integration.find({ workspace: workspaceId, - isActive: true, // TODO: filter so Integrations are ones with non-null apps + isActive: true, app: { $ne: null } }).populate<{integrationAuth: IIntegrationAuth}>('integrationAuth', 'accessToken'); @@ -126,11 +148,13 @@ const syncIntegrationsHelper = async ({ await syncSecrets({ integration: integration.integration, app: integration.app, + target: integration.target, secrets, accessToken }); } } catch (err) { + console.log('syncIntegrationsHelper error', err); Sentry.setUser(null); Sentry.captureException(err); throw new Error('Failed to sync secrets to integrations'); diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index c40d9f5ee5..f255214817 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -2,7 +2,9 @@ import axios from 'axios'; import * as Sentry from '@sentry/node'; import { INTEGRATION_HEROKU, - INTEGRATION_HEROKU_APPS_URL + INTEGRATION_VERCEL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL } from '../variables'; /** @@ -28,6 +30,11 @@ const getApps = async ({ accessToken }); break; + case INTEGRATION_VERCEL: + apps = await getAppsVercel({ + accessToken + }); + break; } } catch (err) { @@ -53,14 +60,14 @@ const getAppsHeroku = async ({ }) => { let apps; try { - const res = await axios.get(INTEGRATION_HEROKU_APPS_URL, { + const res = (await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { headers: { Accept: 'application/vnd.heroku+json; version=3', Authorization: `Bearer ${accessToken}` } - }); + })).data; - apps = res.data.map((a: any) => ({ + apps = res.map((a: any) => ({ name: a.name })); } catch (err) { @@ -72,6 +79,38 @@ const getAppsHeroku = async ({ return apps; } +/** + * Return list of names of apps for Vercel integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Heroku API + * @returns {Object[]} apps - names of Heroku apps + * @returns {String} apps.name - name of Heroku app + */ +const getAppsVercel = async ({ + accessToken +}: { + accessToken: string; +}) => { + let apps; + try { + const res = (await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + })).data; + + apps = res.projects.map((a: any) => ({ + name: a.name + })); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get Vercel integration apps'); + } + + return apps; +} + export { getApps } \ No newline at end of file diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index 1452c4e2c3..9ce670144b 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -2,13 +2,35 @@ import axios from 'axios'; import * as Sentry from '@sentry/node'; import { INTEGRATION_HEROKU, + INTEGRATION_VERCEL, INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL, ACTION_PUSH_TO_HEROKU } from '../variables'; import { - OAUTH_CLIENT_SECRET_HEROKU + SITE_URL, + OAUTH_CLIENT_SECRET_HEROKU, + CLIENT_ID_VERCEL, + CLIENT_SECRET_VERCEL } from '../config'; +interface ExchangeCodeHerokuResponse { + token_type: string; + access_token: string; + expires_in: number; + refresh_token: string; + user_id: string; + session_nonce?: string; +} + +interface ExchangeCodeVercelResponse { + token_type: string; + access_token: string; + installation_id: string; + user_id: string; + team_id?: string; +} + /** * Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2 * code-token exchange for integration named [integration] @@ -37,6 +59,10 @@ const exchangeCode = async ({ code }); break; + case INTEGRATION_VERCEL: + obj = await exchangeCodeVercel({ + code + }); } } catch (err) { Sentry.setUser(null); @@ -62,20 +88,20 @@ const exchangeCodeHeroku = async ({ }: { code: string; }) => { - let res: any; + let res: ExchangeCodeHerokuResponse; let accessExpiresAt = new Date(); try { - res = await axios.post( + res = (await axios.post( INTEGRATION_HEROKU_TOKEN_URL, new URLSearchParams({ grant_type: 'authorization_code', code: code, client_secret: OAUTH_CLIENT_SECRET_HEROKU } as any) - ); + )).data; accessExpiresAt.setSeconds( - accessExpiresAt.getSeconds() + res.data.expires_in + accessExpiresAt.getSeconds() + res.expires_in ); } catch (err) { Sentry.setUser(null); @@ -84,12 +110,53 @@ const exchangeCodeHeroku = async ({ } return ({ - accessToken: res.data.access_token, - refreshToken: res.data.refresh_token, + accessToken: res.access_token, + refreshToken: res.refresh_token, accessExpiresAt }); } +/** + * Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel + * code-token exchange + * @param {Object} obj1 + * @param {Object} obj1.code - code for code-token exchange + * @returns {Object} obj2 + * @returns {String} obj2.accessToken - access token for Heroku API + * @returns {String} obj2.refreshToken - refresh token for Heroku API + * @returns {Date} obj2.accessExpiresAt - date of expiration for access token + */ +const exchangeCodeVercel = async ({ + code +}: { + code: string; +}) => { + let res: ExchangeCodeVercelResponse; + try { + res = (await axios.post( + INTEGRATION_VERCEL_TOKEN_URL, + new URLSearchParams({ + code: code, + client_id: CLIENT_ID_VERCEL, + client_secret: CLIENT_SECRET_VERCEL, + redirect_uri: `${SITE_URL}/vercel` + } as any) + )).data; + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed OAuth2 code-token exchange with Vercel'); + } + + return ({ + accessToken: res.access_token, + refreshToken: null, + accessExpiresAt: null, + teamId: res.team_id + }); +} + export { exchangeCode } \ No newline at end of file diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 15a61ad669..930721d72c 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1,23 +1,34 @@ import axios from 'axios'; import * as Sentry from '@sentry/node'; -import { INTEGRATION_HEROKU } from '../variables'; +import { + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL +} from '../variables'; + +// TODO: need a helper function in the future to handle integration +// envar priorities (i.e. prioritize secrets within integration or those on Infisical) /** * Sync/push [secrets] to [app] in integration named [integration] * @param {Object} obj * @param {Object} obj.integration - name of integration * @param {Object} obj.app - app in integration + * @param {Object} obj.target - (optional) target (environment) in integration * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessToken - access token for integration */ const syncSecrets = async ({ integration, app, + target, secrets, - accessToken + accessToken, }: { integration: string; app: string; + target: string; secrets: any; accessToken: string; }) => { @@ -30,6 +41,13 @@ const syncSecrets = async ({ accessToken }); break; + case INTEGRATION_VERCEL: + await syncSecretsVercel({ + app, + target, + secrets, + accessToken + }); } } catch (err) { Sentry.setUser(null); @@ -54,13 +72,29 @@ const syncSecretsHeroku = async ({ accessToken: string; }) => { try { + const herokuSecrets = (await axios.get( + `${INTEGRATION_HEROKU_API_URL}/apps/${app}/config-vars`, + { + headers: { + Accept: 'application/vnd.heroku+json; version=3', + Authorization: `Bearer ${accessToken}` + } + } + )).data; + + Object.keys(herokuSecrets).forEach(key => { + if (!(key in secrets)) { + secrets[key] = null; + } + }); + await axios.patch( - `https://api.heroku.com/apps/${app}/config-vars`, + `${INTEGRATION_HEROKU_API_URL}/apps/${app}/config-vars`, secrets, { headers: { Accept: 'application/vnd.heroku+json; version=3', - Authorization: 'Bearer ' + accessToken + Authorization: `Bearer ${accessToken}` } } ); @@ -71,6 +105,159 @@ const syncSecretsHeroku = async ({ } } +/** + * Sync/push [secrets] to Heroku [app] + * @param {Object} obj + * @param {String} obj.app - app in integration + * @param {String} obj.target - (optional) target (environment) in integration + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + */ +const syncSecretsVercel = async ({ + app, + target, + secrets, + accessToken +}: { + app: string; + target: string; + secrets: any; + accessToken: string; +}) => { + + interface VercelSecret { + id?: string; + type: string; + key: string; + value: string; + target: string[]; + } + + try { + // Get all (decrypted) secrets back from Vercel in + // decrypted format + const params = new URLSearchParams({ + decrypt: "true" + }); + + const res = (await Promise.all((await axios.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${app}/env`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + )) + .data + .envs + .filter((secret: VercelSecret) => secret.target.includes(target)) + .map(async (secret: VercelSecret) => (await axios.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${app}/env/${secret.id}`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + + } + )).data) + )).reduce((obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret + }), {}); + + let updateSecrets: VercelSecret[] = []; + let deleteSecrets: VercelSecret[] = []; + let newSecrets: VercelSecret[] = []; + + // Identify secrets to create + Object.keys(secrets).map((key) => { + if (!(key in res)) { + newSecrets.push({ + key: key, + value: secrets[key], + type: 'encrypted', + target: [target] + }); + } + }); + + // Identify secrets to update and delete + Object.keys(res).map((key) => { + if (key in secrets) { + if (res[key].value !== secrets[key]) { + // case: secret value has changed + updateSecrets.push({ + id: res[key].id, + key: key, + value: secrets[key], + type: 'encrypted', + target: [target] + }); + } + } else { + // case: secret has been deleted + deleteSecrets.push({ + id: res[key].id, + key: key, + value: res[key].value, + type: 'encrypted', + target: [target], + }); + } + }); + + // Sync/push new secrets + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_VERCEL_API_URL}/v10/projects/${app}/env`, + newSecrets, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + } + + // Sync/push updated secrets + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: VercelSecret) => { + const { + id, + ...updatedSecret + } = secret; + await axios.patch( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${app}/env/${secret.id}`, + updatedSecret, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } + + // Delete secrets + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (secret: VercelSecret) => { + await axios.delete( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${app}/env/${secret.id}`, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to Vercel'); + } +} + export { syncSecrets } \ No newline at end of file diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 90e3f3019e..eb82b95caf 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -5,6 +5,7 @@ import { ENV_STAGING, ENV_PROD, INTEGRATION_HEROKU, + INTEGRATION_VERCEL, INTEGRATION_NETLIFY } from '../variables'; @@ -14,6 +15,7 @@ export interface IIntegration { environment: 'dev' | 'test' | 'staging' | 'prod'; isActive: boolean; app: string; + target: string; integration: 'heroku' | 'netlify'; integrationAuth: Types.ObjectId; } @@ -34,14 +36,21 @@ const integrationSchema = new Schema( type: Boolean, required: true }, - app: { - // name of app in provider + app: { // name of app in provider + type: String, + default: null + }, + target: { // vercel-specific target (environment) type: String, default: null }, integration: { type: String, - enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY], + enum: [ + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY + ], required: true }, integrationAuth: { diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 0e9542a20b..b03a73e52d 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -1,10 +1,15 @@ import { Schema, model, Types } from 'mongoose'; -import { INTEGRATION_HEROKU, INTEGRATION_NETLIFY } from '../variables'; +import { + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY +} from '../variables'; export interface IIntegrationAuth { _id: Types.ObjectId; workspace: Types.ObjectId; integration: 'heroku' | 'netlify'; + teamId: string; refreshCiphertext?: string; refreshIV?: string; refreshTag?: string; @@ -22,9 +27,16 @@ const integrationAuthSchema = new Schema( }, integration: { type: String, - enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY], + enum: [ + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY + ], required: true }, + teamId: { // vercel-specific integration param set at OAuth2 code-token exchange + type: String + }, refreshCiphertext: { type: String, select: false diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index 9f8bcbd9c0..78773e6652 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -7,11 +7,14 @@ import { } from './environment'; import { INTEGRATION_HEROKU, + INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_SET, INTEGRATION_OAUTH2, INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_HEROKU_APPS_URL + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL } from './integration'; import { OWNER, @@ -56,11 +59,14 @@ export { ENV_PROD, ENV_SET, INTEGRATION_HEROKU, + INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_SET, INTEGRATION_OAUTH2, INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_HEROKU_APPS_URL, + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS, ACTION_PUSH_TO_HEROKU diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 6e4fe45b5c..188bead386 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -1,22 +1,32 @@ // integrations const INTEGRATION_HEROKU = 'heroku'; +const INTEGRATION_VERCEL = 'vercel'; const INTEGRATION_NETLIFY = 'netlify'; -const INTEGRATION_SET = new Set([INTEGRATION_HEROKU, INTEGRATION_NETLIFY]); +const INTEGRATION_SET = new Set([ + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY +]); // integration types const INTEGRATION_OAUTH2 = 'oauth2'; // integration oauth endpoints const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token'; +const INTEGRATION_VERCEL_TOKEN_URL = 'https://api.vercel.com/v2/oauth/access_token'; // integration apps endpoints -const INTEGRATION_HEROKU_APPS_URL = 'https://api.heroku.com/apps'; +const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com'; +const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com'; export { INTEGRATION_HEROKU, + INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_SET, INTEGRATION_OAUTH2, INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_HEROKU_APPS_URL + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, } \ No newline at end of file diff --git a/frontend/components/integrations/CloudIntegration.tsx b/frontend/components/integrations/CloudIntegration.tsx index 9fb7213dfc..f3f1ad153d 100644 --- a/frontend/components/integrations/CloudIntegration.tsx +++ b/frontend/components/integrations/CloudIntegration.tsx @@ -33,8 +33,6 @@ const CloudIntegration = ({ integrationOptionPress, integrationAuths }: Props) => { - console.log('cloudIntegrationOption', cloudIntegrationOption); - console.log('integrationAuths', integrationAuths); return integrationAuths ? (
{ const tempApps = await getIntegrationApps({ @@ -48,54 +50,70 @@ const Integration = ({ setIntegrationApp( integration.app ? integration.app : tempAppNames[0] ); + setIntegrationTarget("Development"); }, []); - - return (integrationApp && apps.length > 0) ? ( -
-
-
-
-
- ENVIRONMENT -
- -
+ + if (!integrationApp || apps.length === 0) return
+ + return ( +
+
+
+

ENVIRONMENT

+ +
+
-
-
- INTEGRATION -
-
- {integration.integration.charAt(0).toUpperCase() + - integration.integration.slice(1)} -
+ /> +
+
+

+ INTEGRATION +

+
+ {integration.integration.charAt(0).toUpperCase() + + integration.integration.slice(1)}
+
+
+
+ APP +
+ +
+ {integration.integration === "vercel" && (
-
- HEROKU APP +
+ ENVIRONMENT
-
-
+ )} +
+
{integration.isActive ? (
-
- ) : ( -
- ) + ); }; export default Integration; \ No newline at end of file diff --git a/frontend/components/integrations/IntegrationSection.tsx b/frontend/components/integrations/IntegrationSection.tsx index 1c521f8071..cfa43b4ef3 100644 --- a/frontend/components/integrations/IntegrationSection.tsx +++ b/frontend/components/integrations/IntegrationSection.tsx @@ -16,7 +16,7 @@ const ProjectIntegrationSection = ({ return integrations.length > 0 ? (
-

Current Project Integrations

+

Current Integrations

Manage your integrations of Infisical with third-party services.

diff --git a/frontend/pages/api/integrations/updateIntegration.js b/frontend/pages/api/integrations/updateIntegration.js index db833caf13..cb5288f5ca 100644 --- a/frontend/pages/api/integrations/updateIntegration.js +++ b/frontend/pages/api/integrations/updateIntegration.js @@ -9,13 +9,15 @@ import SecurityClient from "~/utilities/SecurityClient"; * @param {String} obj.app - name of app * @param {String} obj.environment - project environment to push secrets from * @param {Boolean} obj.isActive - active state + * @param {String} obj.target - (optional) target (environment) * @returns */ const updateIntegration = ({ integrationId, app, - environment, - isActive + environment, + isActive, + target }) => { return SecurityClient.fetchCall( "/api/v1/integration/" + integrationId, @@ -27,7 +29,8 @@ const updateIntegration = ({ body: JSON.stringify({ app, environment, - isActive + isActive, + target }), } ).then(async (res) => { diff --git a/frontend/pages/heroku.js b/frontend/pages/heroku.js index 298fee08f2..088c96500e 100644 --- a/frontend/pages/heroku.js +++ b/frontend/pages/heroku.js @@ -16,17 +16,17 @@ export default function Heroku() { // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(async () => { try { - if (state == localStorage.getItem("latestCSRFToken")) { + if (state === localStorage.getItem('latestCSRFToken')) { + localStorage.removeItem('latestCSRFToken'); await AuthorizeIntegration({ - workspaceId: localStorage.getItem("projectData.id"), + workspaceId: localStorage.getItem('projectData.id'), code, integration: "heroku", }); router.push("/integrations/" + localStorage.getItem("projectData.id")); } } catch (error) { - console.error(error); - console.log("Error - Not logged in yet"); + console.error('Heroku integration error: ', error); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/frontend/pages/integrations/[id].js b/frontend/pages/integrations/[id].js index 388b6a5dee..955d502016 100644 --- a/frontend/pages/integrations/[id].js +++ b/frontend/pages/integrations/[id].js @@ -113,7 +113,7 @@ export default function Integrations() { * @returns */ const handleIntegrationOption = async ({ integrationOption }) => { - // TODO: modularize + // TODO: modularize and handle switch by slug // generate CSRF token for OAuth2 code-token exchange integrations const csrfToken = crypto.randomBytes(16).toString("hex"); @@ -121,8 +121,13 @@ export default function Integrations() { switch (integrationOption.name) { case 'Heroku': - window.location = `https://id.heroku.com/oauth/authorize?client_id=7b1311a1-1cb2-4938-8adf-f37a399ec41b&response_type=code&scope=write-protected&state=${csrfToken}`; - return; + // console.log('Heroku integration ', integrationOption); + window.location = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${csrfToken}`; + break; + case 'Vercel': + console.log('Vercel integration ', integrationOption); + window.location = `https://vercel.com/integrations/infisical/new?state=${csrfToken}`; + break; } } diff --git a/frontend/pages/vercel.js b/frontend/pages/vercel.js index cf317ec0fb..7005a25900 100644 --- a/frontend/pages/vercel.js +++ b/frontend/pages/vercel.js @@ -7,17 +7,40 @@ import AuthorizeIntegration from "./api/integrations/authorizeIntegration"; export default function Vercel() { const router = useRouter(); const parsedUrl = queryString.parse(router.asPath.split("?")[1]); + const code = parsedUrl.code; + const state = parsedUrl.state + + // modify comment here /** * Here we forward to the default workspace if a user opens this url */ // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(async () => { - console.log('parsedUrl, xxx', parsedUrl); + console.log('parsedUrl for vercel ', parsedUrl); + if (state === localStorage.getItem('latestCSRFToken')) { + localStorage.removeItem('latestCSRFToken'); + + console.log('integ'); + console.log('code', code); + console.log('state', state); + + await AuthorizeIntegration({ + workspaceId: localStorage.getItem('projectData.id'), + code, + integration: "vercel" + }); + + router.push("/integrations/" + localStorage.getItem("projectData.id")); + } + // parsedUrl.code + // parsedUrl.configurationId + // parsedUrl.next + // parsedUrl.state try { - + } catch (err) { - + console.error('Vercel integration error: ', err); } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/public/json/cloudIntegrations.json b/frontend/public/json/cloudIntegrations.json index 48cecec34d..696be33de8 100644 --- a/frontend/public/json/cloudIntegrations.json +++ b/frontend/public/json/cloudIntegrations.json @@ -5,14 +5,14 @@ "image": "Heroku", "isAvailable": true, "type": "oauth2", - "clientId": "bc132901-935a-4590-b010-f1857efc380d" + "clientId": "7b1311a1-1cb2-4938-8adf-f37a399ec41b" }, { - "name": "Netlify", + "name": "Vercel", "slug": "netlify", "image": "Netlify", - "isAvailable": false, - "type": "oauth2", + "isAvailable": true, + "type": "vercel", "clientId": "" }, {