mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 16:28:11 +00:00
Preliminary Vercel integration
This commit is contained in:
@@ -11,7 +11,8 @@ const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
|
|||||||
const MONGO_URL = process.env.MONGO_URL!;
|
const MONGO_URL = process.env.MONGO_URL!;
|
||||||
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
||||||
const OAUTH_CLIENT_SECRET_HEROKU = process.env.OAUTH_CLIENT_SECRET_HEROKU!;
|
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_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
|
||||||
const POSTHOG_PROJECT_API_KEY =
|
const POSTHOG_PROJECT_API_KEY =
|
||||||
process.env.POSTHOG_PROJECT_API_KEY! ||
|
process.env.POSTHOG_PROJECT_API_KEY! ||
|
||||||
@@ -47,7 +48,8 @@ export {
|
|||||||
MONGO_URL,
|
MONGO_URL,
|
||||||
NODE_ENV,
|
NODE_ENV,
|
||||||
OAUTH_CLIENT_SECRET_HEROKU,
|
OAUTH_CLIENT_SECRET_HEROKU,
|
||||||
OAUTH_TOKEN_URL_HEROKU,
|
CLIENT_SECRET_VERCEL,
|
||||||
|
CLIENT_ID_VERCEL,
|
||||||
POSTHOG_HOST,
|
POSTHOG_HOST,
|
||||||
POSTHOG_PROJECT_API_KEY,
|
POSTHOG_PROJECT_API_KEY,
|
||||||
PRIVATE_KEY,
|
PRIVATE_KEY,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import axios from 'axios';
|
|||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { IntegrationAuth, Integration } from '../models';
|
import { IntegrationAuth, Integration } from '../models';
|
||||||
import { INTEGRATION_SET, ENV_DEV } from '../variables';
|
import { INTEGRATION_SET, ENV_DEV } from '../variables';
|
||||||
import { OAUTH_CLIENT_SECRET_HEROKU, OAUTH_TOKEN_URL_HEROKU } from '../config';
|
|
||||||
import { IntegrationService } from '../services';
|
import { IntegrationService } from '../services';
|
||||||
import { getApps } from '../integrations';
|
import { getApps } from '../integrations';
|
||||||
|
|
||||||
|
|||||||
@@ -31,17 +31,21 @@ interface PushSecret {
|
|||||||
export const updateIntegration = async (req: Request, res: Response) => {
|
export const updateIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
let integration;
|
||||||
|
|
||||||
|
// TODO: add integration-specific validation to ensure that each
|
||||||
|
// integration has the correct fields populated in [Integration]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { app, environment, isActive } = req.body;
|
const { app, environment, isActive, target } = req.body;
|
||||||
|
|
||||||
integration = await Integration.findOneAndUpdate(
|
integration = await Integration.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
_id: req.integration._id
|
_id: req.integration._id
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
app,
|
|
||||||
environment,
|
environment,
|
||||||
isActive
|
isActive,
|
||||||
|
app,
|
||||||
|
target
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
@@ -49,7 +53,6 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (integration) {
|
if (integration) {
|
||||||
|
|
||||||
// trigger event - push secrets
|
// trigger event - push secrets
|
||||||
EventService.handleEvent({
|
EventService.handleEvent({
|
||||||
event: eventPushSecrets({
|
event: eventPushSecrets({
|
||||||
|
|||||||
@@ -10,9 +10,16 @@ import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
|||||||
import { BotService, IntegrationService } from '../services';
|
import { BotService, IntegrationService } from '../services';
|
||||||
import {
|
import {
|
||||||
ENV_DEV,
|
ENV_DEV,
|
||||||
EVENT_PUSH_SECRETS
|
EVENT_PUSH_SECRETS,
|
||||||
|
INTEGRATION_VERCEL
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
|
interface Update {
|
||||||
|
workspace: string;
|
||||||
|
integration: string;
|
||||||
|
teamId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
||||||
* named [integration]
|
* named [integration]
|
||||||
@@ -49,29 +56,45 @@ const handleOAuthExchangeHelper = async ({
|
|||||||
code
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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({
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
integration
|
integration
|
||||||
}, {
|
}, update, {
|
||||||
workspace: workspaceId,
|
|
||||||
integration
|
|
||||||
}, {
|
|
||||||
new: true,
|
new: true,
|
||||||
upsert: true
|
upsert: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// set integration auth refresh token
|
if (res.refreshToken) {
|
||||||
await setIntegrationAuthRefreshHelper({
|
// case: refresh token returned from exchange
|
||||||
integrationAuthId: integrationAuth._id.toString(),
|
// set integration auth refresh token
|
||||||
refreshToken: res.refreshToken
|
await setIntegrationAuthRefreshHelper({
|
||||||
});
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
refreshToken: res.refreshToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// set integration auth access token
|
if (res.accessToken) {
|
||||||
await setIntegrationAuthAccessHelper({
|
// case: access token returned from exchange
|
||||||
integrationAuthId: integrationAuth._id.toString(),
|
// set integration auth access token
|
||||||
accessToken: res.accessToken,
|
await setIntegrationAuthAccessHelper({
|
||||||
accessExpiresAt: res.accessExpiresAt
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
});
|
accessToken: res.accessToken,
|
||||||
|
accessExpiresAt: res.accessExpiresAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// initialize new integration after exchange
|
// initialize new integration after exchange
|
||||||
await new Integration({
|
await new Integration({
|
||||||
@@ -82,7 +105,6 @@ const handleOAuthExchangeHelper = async ({
|
|||||||
integration,
|
integration,
|
||||||
integrationAuth: integrationAuth._id
|
integrationAuth: integrationAuth._id
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -104,7 +126,7 @@ const syncIntegrationsHelper = async ({
|
|||||||
try {
|
try {
|
||||||
integrations = await Integration.find({
|
integrations = await Integration.find({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
isActive: true, // TODO: filter so Integrations are ones with non-null apps
|
isActive: true,
|
||||||
app: { $ne: null }
|
app: { $ne: null }
|
||||||
}).populate<{integrationAuth: IIntegrationAuth}>('integrationAuth', 'accessToken');
|
}).populate<{integrationAuth: IIntegrationAuth}>('integrationAuth', 'accessToken');
|
||||||
|
|
||||||
@@ -126,11 +148,13 @@ const syncIntegrationsHelper = async ({
|
|||||||
await syncSecrets({
|
await syncSecrets({
|
||||||
integration: integration.integration,
|
integration: integration.integration,
|
||||||
app: integration.app,
|
app: integration.app,
|
||||||
|
target: integration.target,
|
||||||
secrets,
|
secrets,
|
||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.log('syncIntegrationsHelper error', err);
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to sync secrets to integrations');
|
throw new Error('Failed to sync secrets to integrations');
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import axios from 'axios';
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import {
|
import {
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_HEROKU_APPS_URL
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_HEROKU_API_URL,
|
||||||
|
INTEGRATION_VERCEL_API_URL
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,6 +30,11 @@ const getApps = async ({
|
|||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
apps = await getAppsVercel({
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -53,14 +60,14 @@ const getAppsHeroku = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
let apps;
|
let apps;
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(INTEGRATION_HEROKU_APPS_URL, {
|
const res = (await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/vnd.heroku+json; version=3',
|
Accept: 'application/vnd.heroku+json; version=3',
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
});
|
})).data;
|
||||||
|
|
||||||
apps = res.data.map((a: any) => ({
|
apps = res.map((a: any) => ({
|
||||||
name: a.name
|
name: a.name
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -72,6 +79,38 @@ const getAppsHeroku = async ({
|
|||||||
return apps;
|
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 {
|
export {
|
||||||
getApps
|
getApps
|
||||||
}
|
}
|
||||||
@@ -2,13 +2,35 @@ import axios from 'axios';
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import {
|
import {
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
ACTION_PUSH_TO_HEROKU
|
ACTION_PUSH_TO_HEROKU
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import {
|
import {
|
||||||
OAUTH_CLIENT_SECRET_HEROKU
|
SITE_URL,
|
||||||
|
OAUTH_CLIENT_SECRET_HEROKU,
|
||||||
|
CLIENT_ID_VERCEL,
|
||||||
|
CLIENT_SECRET_VERCEL
|
||||||
} from '../config';
|
} 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
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
|
||||||
* code-token exchange for integration named [integration]
|
* code-token exchange for integration named [integration]
|
||||||
@@ -37,6 +59,10 @@ const exchangeCode = async ({
|
|||||||
code
|
code
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
obj = await exchangeCodeVercel({
|
||||||
|
code
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
@@ -62,20 +88,20 @@ const exchangeCodeHeroku = async ({
|
|||||||
}: {
|
}: {
|
||||||
code: string;
|
code: string;
|
||||||
}) => {
|
}) => {
|
||||||
let res: any;
|
let res: ExchangeCodeHerokuResponse;
|
||||||
let accessExpiresAt = new Date();
|
let accessExpiresAt = new Date();
|
||||||
try {
|
try {
|
||||||
res = await axios.post(
|
res = (await axios.post(
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
code: code,
|
code: code,
|
||||||
client_secret: OAUTH_CLIENT_SECRET_HEROKU
|
client_secret: OAUTH_CLIENT_SECRET_HEROKU
|
||||||
} as any)
|
} as any)
|
||||||
);
|
)).data;
|
||||||
|
|
||||||
accessExpiresAt.setSeconds(
|
accessExpiresAt.setSeconds(
|
||||||
accessExpiresAt.getSeconds() + res.data.expires_in
|
accessExpiresAt.getSeconds() + res.expires_in
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
@@ -84,12 +110,53 @@ const exchangeCodeHeroku = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ({
|
return ({
|
||||||
accessToken: res.data.access_token,
|
accessToken: res.access_token,
|
||||||
refreshToken: res.data.refresh_token,
|
refreshToken: res.refresh_token,
|
||||||
accessExpiresAt
|
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 {
|
export {
|
||||||
exchangeCode
|
exchangeCode
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,34 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as Sentry from '@sentry/node';
|
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]
|
* Sync/push [secrets] to [app] in integration named [integration]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Object} obj.integration - name of integration
|
* @param {Object} obj.integration - name of integration
|
||||||
* @param {Object} obj.app - app in 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 {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
|
* @param {String} obj.accessToken - access token for integration
|
||||||
*/
|
*/
|
||||||
const syncSecrets = async ({
|
const syncSecrets = async ({
|
||||||
integration,
|
integration,
|
||||||
app,
|
app,
|
||||||
|
target,
|
||||||
secrets,
|
secrets,
|
||||||
accessToken
|
accessToken,
|
||||||
}: {
|
}: {
|
||||||
integration: string;
|
integration: string;
|
||||||
app: string;
|
app: string;
|
||||||
|
target: string;
|
||||||
secrets: any;
|
secrets: any;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -30,6 +41,13 @@ const syncSecrets = async ({
|
|||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
await syncSecretsVercel({
|
||||||
|
app,
|
||||||
|
target,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
@@ -54,13 +72,29 @@ const syncSecretsHeroku = async ({
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
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(
|
await axios.patch(
|
||||||
`https://api.heroku.com/apps/${app}/config-vars`,
|
`${INTEGRATION_HEROKU_API_URL}/apps/${app}/config-vars`,
|
||||||
secrets,
|
secrets,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/vnd.heroku+json; version=3',
|
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 {
|
export {
|
||||||
syncSecrets
|
syncSecrets
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ENV_STAGING,
|
ENV_STAGING,
|
||||||
ENV_PROD,
|
ENV_PROD,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY
|
INTEGRATION_NETLIFY
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ export interface IIntegration {
|
|||||||
environment: 'dev' | 'test' | 'staging' | 'prod';
|
environment: 'dev' | 'test' | 'staging' | 'prod';
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
app: string;
|
app: string;
|
||||||
|
target: string;
|
||||||
integration: 'heroku' | 'netlify';
|
integration: 'heroku' | 'netlify';
|
||||||
integrationAuth: Types.ObjectId;
|
integrationAuth: Types.ObjectId;
|
||||||
}
|
}
|
||||||
@@ -34,14 +36,21 @@ const integrationSchema = new Schema<IIntegration>(
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
app: {
|
app: { // name of app in provider
|
||||||
// name of app in provider
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
target: { // vercel-specific target (environment)
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
integration: {
|
integration: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY],
|
enum: [
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY
|
||||||
|
],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
integrationAuth: {
|
integrationAuth: {
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
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 {
|
export interface IIntegrationAuth {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
integration: 'heroku' | 'netlify';
|
integration: 'heroku' | 'netlify';
|
||||||
|
teamId: string;
|
||||||
refreshCiphertext?: string;
|
refreshCiphertext?: string;
|
||||||
refreshIV?: string;
|
refreshIV?: string;
|
||||||
refreshTag?: string;
|
refreshTag?: string;
|
||||||
@@ -22,9 +27,16 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
|||||||
},
|
},
|
||||||
integration: {
|
integration: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY],
|
enum: [
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY
|
||||||
|
],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
teamId: { // vercel-specific integration param set at OAuth2 code-token exchange
|
||||||
|
type: String
|
||||||
|
},
|
||||||
refreshCiphertext: {
|
refreshCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import {
|
|||||||
} from './environment';
|
} from './environment';
|
||||||
import {
|
import {
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
INTEGRATION_HEROKU_APPS_URL
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
|
INTEGRATION_HEROKU_API_URL,
|
||||||
|
INTEGRATION_VERCEL_API_URL
|
||||||
} from './integration';
|
} from './integration';
|
||||||
import {
|
import {
|
||||||
OWNER,
|
OWNER,
|
||||||
@@ -56,11 +59,14 @@ export {
|
|||||||
ENV_PROD,
|
ENV_PROD,
|
||||||
ENV_SET,
|
ENV_SET,
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
INTEGRATION_HEROKU_APPS_URL,
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
|
INTEGRATION_HEROKU_API_URL,
|
||||||
|
INTEGRATION_VERCEL_API_URL,
|
||||||
EVENT_PUSH_SECRETS,
|
EVENT_PUSH_SECRETS,
|
||||||
EVENT_PULL_SECRETS,
|
EVENT_PULL_SECRETS,
|
||||||
ACTION_PUSH_TO_HEROKU
|
ACTION_PUSH_TO_HEROKU
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
// integrations
|
// integrations
|
||||||
const INTEGRATION_HEROKU = 'heroku';
|
const INTEGRATION_HEROKU = 'heroku';
|
||||||
|
const INTEGRATION_VERCEL = 'vercel';
|
||||||
const INTEGRATION_NETLIFY = 'netlify';
|
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
|
// integration types
|
||||||
const INTEGRATION_OAUTH2 = 'oauth2';
|
const INTEGRATION_OAUTH2 = 'oauth2';
|
||||||
|
|
||||||
// integration oauth endpoints
|
// integration oauth endpoints
|
||||||
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
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
|
// 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 {
|
export {
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
INTEGRATION_HEROKU_APPS_URL
|
INTEGRATION_VERCEL_TOKEN_URL,
|
||||||
|
INTEGRATION_HEROKU_API_URL,
|
||||||
|
INTEGRATION_VERCEL_API_URL,
|
||||||
}
|
}
|
||||||
@@ -33,8 +33,6 @@ const CloudIntegration = ({
|
|||||||
integrationOptionPress,
|
integrationOptionPress,
|
||||||
integrationAuths
|
integrationAuths
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
console.log('cloudIntegrationOption', cloudIntegrationOption);
|
|
||||||
console.log('integrationAuths', integrationAuths);
|
|
||||||
return integrationAuths ? (
|
return integrationAuths ? (
|
||||||
<div
|
<div
|
||||||
className={`relative ${
|
className={`relative ${
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import {
|
||||||
reverseEnvMapping
|
envMapping,
|
||||||
|
reverseEnvMapping
|
||||||
} from "../../public/data/frequentConstants";
|
} from "../../public/data/frequentConstants";
|
||||||
import updateIntegration from "../../pages/api/integrations/updateIntegration"
|
import updateIntegration from "../../pages/api/integrations/updateIntegration"
|
||||||
import deleteIntegration from "../../pages/api/integrations/DeleteIntegration"
|
import deleteIntegration from "../../pages/api/integrations/DeleteIntegration"
|
||||||
@@ -37,6 +38,7 @@ const Integration = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [apps, setApps] = useState([]);
|
const [apps, setApps] = useState([]);
|
||||||
const [integrationApp, setIntegrationApp] = useState(null);
|
const [integrationApp, setIntegrationApp] = useState(null);
|
||||||
|
const [integrationTarget, setIntegrationTarget] = useState(null);
|
||||||
|
|
||||||
useEffect(async () => {
|
useEffect(async () => {
|
||||||
const tempApps = await getIntegrationApps({
|
const tempApps = await getIntegrationApps({
|
||||||
@@ -48,54 +50,70 @@ const Integration = ({
|
|||||||
setIntegrationApp(
|
setIntegrationApp(
|
||||||
integration.app ? integration.app : tempAppNames[0]
|
integration.app ? integration.app : tempAppNames[0]
|
||||||
);
|
);
|
||||||
|
setIntegrationTarget("Development");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (integrationApp && apps.length > 0) ? (
|
if (!integrationApp || apps.length === 0) return <div></div>
|
||||||
<div className="flex flex-col max-w-5xl justify-center bg-white/5 p-6 rounded-md mx-6 mt-8">
|
|
||||||
<div className="relative px-4 flex flex-row items-center justify-between mb-4">
|
return (
|
||||||
<div className="flex flex-row">
|
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
|
||||||
<div>
|
<div className="flex">
|
||||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
<div>
|
||||||
ENVIRONMENT
|
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p>
|
||||||
</div>
|
<ListBox data={!integration.isActive ? [
|
||||||
<ListBox
|
"Development",
|
||||||
data={
|
"Staging",
|
||||||
!integration.isActive && [
|
"Testing",
|
||||||
"Development",
|
"Production",
|
||||||
"Staging",
|
] : null}
|
||||||
"Testing",
|
selected={integrationEnvironment}
|
||||||
"Production",
|
onChange={setIntegrationEnvironment}
|
||||||
]
|
isFull={true}
|
||||||
}
|
/>
|
||||||
selected={integrationEnvironment}
|
</div>
|
||||||
onChange={setIntegrationEnvironment}
|
<div className="pt-2">
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faArrowRight}
|
icon={faArrowRight}
|
||||||
className="mx-4 text-gray-400 mt-8"
|
className="mx-4 text-gray-400 mt-8"
|
||||||
/>
|
/>
|
||||||
<div className="mr-2">
|
</div>
|
||||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
<div className="mr-2">
|
||||||
INTEGRATION
|
<p className="text-gray-400 text-xs font-semibold mb-2">
|
||||||
</div>
|
INTEGRATION
|
||||||
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-20 text-sm font-semibold text-gray-300">
|
</p>
|
||||||
{integration.integration.charAt(0).toUpperCase() +
|
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300">
|
||||||
integration.integration.slice(1)}
|
{integration.integration.charAt(0).toUpperCase() +
|
||||||
</div>
|
integration.integration.slice(1)}
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
|
||||||
HEROKU APP
|
|
||||||
</div>
|
|
||||||
<ListBox
|
|
||||||
data={!integration.isActive && apps}
|
|
||||||
selected={integrationApp}
|
|
||||||
onChange={setIntegrationApp}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row mt-6">
|
<div className="mr-2">
|
||||||
|
<div className="text-gray-400 text-xs font-semibold mb-2">
|
||||||
|
APP
|
||||||
|
</div>
|
||||||
|
<ListBox
|
||||||
|
data={!integration.isActive && apps}
|
||||||
|
selected={integrationApp}
|
||||||
|
onChange={setIntegrationApp}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{integration.integration === "vercel" && (
|
||||||
|
<div>
|
||||||
|
<div className="text-gray-400 text-xs font-semibold mb-2">
|
||||||
|
ENVIRONMENT
|
||||||
|
</div>
|
||||||
|
<ListBox
|
||||||
|
data={!integration.isActive && [
|
||||||
|
"Production",
|
||||||
|
"Preview",
|
||||||
|
"Development"
|
||||||
|
]}
|
||||||
|
selected={integrationTarget}
|
||||||
|
onChange={setIntegrationTarget}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
{integration.isActive ? (
|
{integration.isActive ? (
|
||||||
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
|
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
@@ -112,7 +130,8 @@ const Integration = ({
|
|||||||
integrationId: integration._id,
|
integrationId: integration._id,
|
||||||
environment: envMapping[integrationEnvironment],
|
environment: envMapping[integrationEnvironment],
|
||||||
app: integrationApp,
|
app: integrationApp,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
target: integrationTarget.toLowerCase()
|
||||||
});
|
});
|
||||||
router.reload();
|
router.reload();
|
||||||
}}
|
}}
|
||||||
@@ -133,12 +152,9 @@ const Integration = ({
|
|||||||
icon={faX}
|
icon={faX}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
);
|
||||||
<div></div>
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Integration;
|
export default Integration;
|
||||||
@@ -16,7 +16,7 @@ const ProjectIntegrationSection = ({
|
|||||||
return integrations.length > 0 ? (
|
return integrations.length > 0 ? (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
<div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2">
|
<div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2">
|
||||||
<h1 className="font-semibold text-3xl">Current Project Integrations</h1>
|
<h1 className="font-semibold text-3xl">Current Integrations</h1>
|
||||||
<p className="text-base text-gray-400">
|
<p className="text-base text-gray-400">
|
||||||
Manage your integrations of Infisical with third-party services.
|
Manage your integrations of Infisical with third-party services.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ import SecurityClient from "~/utilities/SecurityClient";
|
|||||||
* @param {String} obj.app - name of app
|
* @param {String} obj.app - name of app
|
||||||
* @param {String} obj.environment - project environment to push secrets from
|
* @param {String} obj.environment - project environment to push secrets from
|
||||||
* @param {Boolean} obj.isActive - active state
|
* @param {Boolean} obj.isActive - active state
|
||||||
|
* @param {String} obj.target - (optional) target (environment)
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const updateIntegration = ({
|
const updateIntegration = ({
|
||||||
integrationId,
|
integrationId,
|
||||||
app,
|
app,
|
||||||
environment,
|
environment,
|
||||||
isActive
|
isActive,
|
||||||
|
target
|
||||||
}) => {
|
}) => {
|
||||||
return SecurityClient.fetchCall(
|
return SecurityClient.fetchCall(
|
||||||
"/api/v1/integration/" + integrationId,
|
"/api/v1/integration/" + integrationId,
|
||||||
@@ -27,7 +29,8 @@ const updateIntegration = ({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
app,
|
app,
|
||||||
environment,
|
environment,
|
||||||
isActive
|
isActive,
|
||||||
|
target
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
).then(async (res) => {
|
).then(async (res) => {
|
||||||
|
|||||||
@@ -16,17 +16,17 @@ export default function Heroku() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
useEffect(async () => {
|
useEffect(async () => {
|
||||||
try {
|
try {
|
||||||
if (state == localStorage.getItem("latestCSRFToken")) {
|
if (state === localStorage.getItem('latestCSRFToken')) {
|
||||||
|
localStorage.removeItem('latestCSRFToken');
|
||||||
await AuthorizeIntegration({
|
await AuthorizeIntegration({
|
||||||
workspaceId: localStorage.getItem("projectData.id"),
|
workspaceId: localStorage.getItem('projectData.id'),
|
||||||
code,
|
code,
|
||||||
integration: "heroku",
|
integration: "heroku",
|
||||||
});
|
});
|
||||||
router.push("/integrations/" + localStorage.getItem("projectData.id"));
|
router.push("/integrations/" + localStorage.getItem("projectData.id"));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error('Heroku integration error: ', error);
|
||||||
console.log("Error - Not logged in yet");
|
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export default function Integrations() {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const handleIntegrationOption = async ({ integrationOption }) => {
|
const handleIntegrationOption = async ({ integrationOption }) => {
|
||||||
// TODO: modularize
|
// TODO: modularize and handle switch by slug
|
||||||
|
|
||||||
// generate CSRF token for OAuth2 code-token exchange integrations
|
// generate CSRF token for OAuth2 code-token exchange integrations
|
||||||
const csrfToken = crypto.randomBytes(16).toString("hex");
|
const csrfToken = crypto.randomBytes(16).toString("hex");
|
||||||
@@ -121,8 +121,13 @@ export default function Integrations() {
|
|||||||
|
|
||||||
switch (integrationOption.name) {
|
switch (integrationOption.name) {
|
||||||
case 'Heroku':
|
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}`;
|
// console.log('Heroku integration ', integrationOption);
|
||||||
return;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,40 @@ import AuthorizeIntegration from "./api/integrations/authorizeIntegration";
|
|||||||
export default function Vercel() {
|
export default function Vercel() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
|
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
|
* Here we forward to the default workspace if a user opens this url
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
useEffect(async () => {
|
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 {
|
try {
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('Vercel integration error: ', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
"image": "Heroku",
|
"image": "Heroku",
|
||||||
"isAvailable": true,
|
"isAvailable": true,
|
||||||
"type": "oauth2",
|
"type": "oauth2",
|
||||||
"clientId": "bc132901-935a-4590-b010-f1857efc380d"
|
"clientId": "7b1311a1-1cb2-4938-8adf-f37a399ec41b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Netlify",
|
"name": "Vercel",
|
||||||
"slug": "netlify",
|
"slug": "netlify",
|
||||||
"image": "Netlify",
|
"image": "Netlify",
|
||||||
"isAvailable": false,
|
"isAvailable": true,
|
||||||
"type": "oauth2",
|
"type": "vercel",
|
||||||
"clientId": ""
|
"clientId": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user