Skip to content

Commit

Permalink
Preliminary Vercel integration
Browse files Browse the repository at this point in the history
  • Loading branch information
dangtony98 committed Dec 13, 2022
1 parent 271c810 commit 3e62392
Show file tree
Hide file tree
Showing 19 changed files with 516 additions and 113 deletions.
6 changes: 4 additions & 2 deletions backend/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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! ||
Expand Down Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion backend/src/controllers/integrationAuthController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
11 changes: 7 additions & 4 deletions backend/src/controllers/integrationController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,28 @@ 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
}
);

if (integration) {

// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
Expand Down
58 changes: 41 additions & 17 deletions backend/src/helpers/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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({
Expand All @@ -82,7 +105,6 @@ const handleOAuthExchangeHelper = async ({
integration,
integrationAuth: integrationAuth._id
}).save();

} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
Expand All @@ -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');

Expand All @@ -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');
Expand Down
47 changes: 43 additions & 4 deletions backend/src/integrations/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -28,6 +30,11 @@ const getApps = async ({
accessToken
});
break;
case INTEGRATION_VERCEL:
apps = await getAppsVercel({
accessToken
});
break;
}

} catch (err) {
Expand All @@ -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) {
Expand All @@ -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
}
81 changes: 74 additions & 7 deletions backend/src/integrations/exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -37,6 +59,10 @@ const exchangeCode = async ({
code
});
break;
case INTEGRATION_VERCEL:
obj = await exchangeCodeVercel({
code
});
}
} catch (err) {
Sentry.setUser(null);
Expand All @@ -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);
Expand All @@ -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
}
Loading

0 comments on commit 3e62392

Please sign in to comment.