Preliminary Vercel integration

This commit is contained in:
Tuan Dang
2022-12-13 13:59:21 -05:00
parent 271c810692
commit 3e623922b4
19 changed files with 521 additions and 118 deletions
+4 -2
View File
@@ -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({
+42 -18
View File
@@ -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');
+43 -4
View File
@@ -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
} }
+74 -7
View File
@@ -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
} }
+191 -4
View File
@@ -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
} }
+12 -3
View File
@@ -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: {
+14 -2
View File
@@ -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
+8 -2
View File
@@ -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
+13 -3
View File
@@ -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) => {
+4 -4
View File
@@ -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
}, []); }, []);
+8 -3
View File
@@ -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;
} }
} }
+26 -3
View File
@@ -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
+4 -4
View File
@@ -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": ""
}, },
{ {