Finish Netlify integration v1 full-loop

This commit is contained in:
Tuan Dang
2022-12-14 18:18:21 -05:00
parent fe17d8459b
commit 787e54fb91
14 changed files with 441 additions and 108 deletions
+61 -8
View File
@@ -1,10 +1,15 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import {
IIntegrationAuth
} from '../models';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL
} from '../variables';
/**
@@ -16,15 +21,21 @@ import {
* @returns {String} apps.name - name of integration app
*/
const getApps = async ({
integration,
integrationAuth,
accessToken
}: {
integration: string;
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let apps;
interface App {
name: string;
siteId?: string;
}
let apps: App[]; // TODO: add type and define payloads for apps
try {
switch (integration) {
switch (integrationAuth.integration) {
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken
@@ -35,6 +46,12 @@ const getApps = async ({
accessToken
});
break;
case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({
integrationAuth,
accessToken
});
break;
}
} catch (err) {
@@ -82,9 +99,9 @@ const getAppsHeroku = async ({
/**
* 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
* @param {String} obj.accessToken - access token for Vercel API
* @returns {Object[]} apps - names of Vercel apps
* @returns {String} apps.name - name of Vercel app
*/
const getAppsVercel = async ({
accessToken
@@ -111,6 +128,42 @@ const getAppsVercel = async ({
return apps;
}
/**
* Return list of names of sites for Netlify integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Netlify API
* @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site
*/
const getAppsNetlify = async ({
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let apps;
try {
const res = (await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
})).data;
apps = res.map((a: any) => ({
name: a.name,
siteId: a.site_id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Netlify integration apps');
}
return apps;
}
export {
getApps
}
+36 -21
View File
@@ -35,6 +35,14 @@ interface ExchangeCodeVercelResponse {
team_id?: string;
}
interface ExchangeCodeNetlifyResponse {
access_token: string;
token_type: string;
refresh_token: string;
scope: string;
created_at: number;
}
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
* code-token exchange for integration named [integration]
@@ -152,7 +160,6 @@ const exchangeCodeVercel = async ({
redirect_uri: `${SITE_URL}/vercel`
} as any)
)).data;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
@@ -182,42 +189,50 @@ const exchangeCodeNetlify = async ({
}: {
code: string;
}) => {
console.log('exchangeCodeNetlify');
let res: ExchangeCodeVercelResponse;
let res: ExchangeCodeNetlifyResponse;
let accountId;
try {
res = (await axios.post(
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: CLIENT_ID_VERCEL,
client_secret: CLIENT_SECRET_VERCEL,
redirect_uri: `${SITE_URL}/vercel`
client_id: CLIENT_ID_NETLIFY,
client_secret: CLIENT_SECRET_NETLIFY,
redirect_uri: `${SITE_URL}/netlify`
} as any)
)).data;
res = (await axios.post(
INTEGRATION_NETLIFY_TOKEN_URL,
`${"https://api.netlify.com/oauth/token"}?code=${code}&client_id=${CLIENT_ID_NETLIFY}&client_secret=${CLIENT_SECRET_NETLIFY}&grant_type=authorization_code&redirect_uri=${SITE_URL}/netlify`
// INTEGRATION_NETLIFY_TOKEN_URL,
// new URLSearchParams({
// code: code,
// client_id: CLIENT_ID_NETLIFY,
// client_secret: CLIENT_SECRET_NETLIFY,
// redirect_uri: `${SITE_URL}/netlify`
// } as any)
));
const res2 = await axios.get(
'https://api.netlify.com/api/v1/sites',
{
headers: {
Authorization: `Bearer ${res.access_token}`
}
}
);
console.log('resss', res);
const res3 = (await axios.get(
'https://api.netlify.com/api/v1/accounts',
{
headers: {
Authorization: `Bearer ${res.access_token}`
}
}
)).data;
accountId = res3[0].id;
} catch (err) {
console.error('netlify err', err);
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Netlify');
}
return ({
accessToken: res.access_token,
refreshToken: res.refresh_token,
accountId
});
}
+197 -31
View File
@@ -1,10 +1,15 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import {
IIntegration, IIntegrationAuth
} from '../models';
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL
} from '../variables';
// TODO: need a helper function in the future to handle integration
@@ -13,7 +18,8 @@ import {
/**
* Sync/push [secrets] to [app] in integration named [integration]
* @param {Object} obj
* @param {Object} obj.integration - name of integration
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @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)
@@ -21,33 +27,39 @@ import {
*/
const syncSecrets = async ({
integration,
app,
target,
integrationAuth,
secrets,
accessToken,
}: {
integration: string;
app: string;
target: string;
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: any;
accessToken: string;
}) => {
try {
switch (integration) {
switch (integration.integration) {
case INTEGRATION_HEROKU:
await syncSecretsHeroku({
app,
integration,
secrets,
accessToken
});
break;
case INTEGRATION_VERCEL:
await syncSecretsVercel({
app,
target,
integration,
secrets,
accessToken
});
break;
case INTEGRATION_NETLIFY:
await syncSecretsNetlify({
integration,
integrationAuth,
secrets,
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
@@ -59,21 +71,21 @@ const syncSecrets = async ({
/**
* Sync/push [secrets] to Heroku [app]
* @param {Object} obj
* @param {String} obj.app - app in integration
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
*/
const syncSecretsHeroku = async ({
app,
integration,
secrets,
accessToken
}: {
app: string;
integration: IIntegration,
secrets: any;
accessToken: string;
}) => {
try {
const herokuSecrets = (await axios.get(
`${INTEGRATION_HEROKU_API_URL}/apps/${app}/config-vars`,
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
{
headers: {
Accept: 'application/vnd.heroku+json; version=3',
@@ -89,7 +101,7 @@ const syncSecretsHeroku = async ({
});
await axios.patch(
`${INTEGRATION_HEROKU_API_URL}/apps/${app}/config-vars`,
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
secrets,
{
headers: {
@@ -108,18 +120,15 @@ 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 {IIntegration} obj.integration - integration details
* @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,
integration,
secrets,
accessToken
}: {
app: string;
target: string;
integration: IIntegration,
secrets: any;
accessToken: string;
}) => {
@@ -140,7 +149,7 @@ const syncSecretsVercel = async ({
});
const res = (await Promise.all((await axios.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${app}/env`,
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
{
params,
headers: {
@@ -150,9 +159,9 @@ const syncSecretsVercel = async ({
))
.data
.envs
.filter((secret: VercelSecret) => secret.target.includes(target))
.filter((secret: VercelSecret) => secret.target.includes(integration.target))
.map(async (secret: VercelSecret) => (await axios.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${app}/env/${secret.id}`,
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{
headers: {
Authorization: `Bearer ${accessToken}`
@@ -172,11 +181,12 @@ const syncSecretsVercel = async ({
// Identify secrets to create
Object.keys(secrets).map((key) => {
if (!(key in res)) {
// case: secret has been created
newSecrets.push({
key: key,
value: secrets[key],
type: 'encrypted',
target: [target]
target: [integration.target]
});
}
});
@@ -191,7 +201,7 @@ const syncSecretsVercel = async ({
key: key,
value: secrets[key],
type: 'encrypted',
target: [target]
target: [integration.target]
});
}
} else {
@@ -201,7 +211,7 @@ const syncSecretsVercel = async ({
key: key,
value: res[key].value,
type: 'encrypted',
target: [target],
target: [integration.target],
});
}
});
@@ -209,7 +219,7 @@ const syncSecretsVercel = async ({
// Sync/push new secrets
if (newSecrets.length > 0) {
await axios.post(
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${app}/env`,
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
newSecrets,
{
headers: {
@@ -227,7 +237,7 @@ const syncSecretsVercel = async ({
...updatedSecret
} = secret;
await axios.patch(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${app}/env/${secret.id}`,
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
updatedSecret,
{
headers: {
@@ -242,7 +252,7 @@ const syncSecretsVercel = async ({
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (secret: VercelSecret) => {
await axios.delete(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${app}/env/${secret.id}`,
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{
headers: {
Authorization: `Bearer ${accessToken}`
@@ -258,6 +268,162 @@ const syncSecretsVercel = async ({
}
}
/**
* Sync/push [secrets] to Netlify site [app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
*/
const syncSecretsNetlify = async ({
integration,
integrationAuth,
secrets,
accessToken
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: any;
accessToken: string;
}) => {
try {
const getParams = new URLSearchParams({
context_name: integration.context,
site_id: integration.siteId
});
const res = (await axios.get(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
{
params: getParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
))
.data
.reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret.values[0].value
}), {});
interface UpdateNetlifySecret {
key: string;
context: string;
value: string;
}
interface DeleteNetlifySecret {
key: string;
}
interface NewNetlifySecretValue {
value: string;
context: string;
}
interface NewNetlifySecret {
key: string;
values: NewNetlifySecretValue[];
}
let updateSecrets: UpdateNetlifySecret[] = [];
let deleteSecrets: DeleteNetlifySecret[] = [];
let newSecrets: NewNetlifySecret[] = [];
// Identify secrets to create
Object.keys(secrets).map((key) => {
if (!(key in res)) {
// case: secret has been created
newSecrets.push({
key: key,
values: [{
value: secrets[key], // include id?
context: integration.context
}]
});
}
});
// Identify secrets to update and delete
Object.keys(res).map((key) => {
if (key in secrets) {
if (res[key] !== secrets[key]) {
// case: secret value has changed
updateSecrets.push({
key: key,
context: integration.context,
value: secrets[key]
});
}
} else {
// case: secret has been deleted
deleteSecrets.push({
key
});
}
});
const syncParams = new URLSearchParams({
site_id: integration.siteId
});
// Sync/push new secrets
if (newSecrets.length > 0) {
await axios.post(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
newSecrets,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
}
// Sync/push updated secrets
if (updateSecrets.length > 0) {
updateSecrets.forEach(async (secret: UpdateNetlifySecret) => {
await axios.patch(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
{
context: secret.context,
value: secret.value
},
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
// Delete secrets
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (secret: DeleteNetlifySecret) => {
await axios.delete(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Heroku');
}
}
export {
syncSecrets
}