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
@@ -51,7 +51,7 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
let apps; let apps;
try { try {
apps = await getApps({ apps = await getApps({
integration: req.integrationAuth.integration, integrationAuth: req.integrationAuth,
accessToken: req.accessToken accessToken: req.accessToken
}); });
} catch (err) { } catch (err) {
@@ -35,8 +35,15 @@ export const updateIntegration = async (req: Request, res: Response) => {
// integration has the correct fields populated in [Integration] // integration has the correct fields populated in [Integration]
try { try {
const { app, environment, isActive, target } = req.body; const {
app,
environment,
isActive,
target, // vercel-specific integration param
context, // netlify-specific integration param
siteId // netlify-specific integration param
} = req.body;
integration = await Integration.findOneAndUpdate( integration = await Integration.findOneAndUpdate(
{ {
_id: req.integration._id _id: req.integration._id
@@ -45,7 +52,9 @@ export const updateIntegration = async (req: Request, res: Response) => {
environment, environment,
isActive, isActive,
app, app,
target target,
context,
siteId
}, },
{ {
new: true new: true
+1
View File
@@ -74,6 +74,7 @@ const getSecretsHelper = async ({
const key = await getKey({ workspaceId }); const key = await getKey({ workspaceId });
const secrets = await Secret.find({ const secrets = await Secret.find({
workspaceId, workspaceId,
environment,
type: SECRET_SHARED type: SECRET_SHARED
}); });
+14 -9
View File
@@ -11,13 +11,15 @@ import { BotService, IntegrationService } from '../services';
import { import {
ENV_DEV, ENV_DEV,
EVENT_PUSH_SECRETS, EVENT_PUSH_SECRETS,
INTEGRATION_VERCEL INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
} from '../variables'; } from '../variables';
interface Update { interface Update {
workspace: string; workspace: string;
integration: string; integration: string;
teamId?: string; teamId?: string;
accountId?: string;
} }
/** /**
@@ -55,8 +57,6 @@ const handleOAuthExchangeHelper = async ({
integration, integration,
code code
}); });
return;
let update: Update = { let update: Update = {
workspace: workspaceId, workspace: workspaceId,
@@ -67,6 +67,9 @@ const handleOAuthExchangeHelper = async ({
case INTEGRATION_VERCEL: case INTEGRATION_VERCEL:
update.teamId = res.teamId; update.teamId = res.teamId;
break; break;
case INTEGRATION_NETLIFY:
update.accountId = res.accountId;
break;
} }
integrationAuth = await IntegrationAuth.findOneAndUpdate({ integrationAuth = await IntegrationAuth.findOneAndUpdate({
@@ -124,11 +127,12 @@ const syncIntegrationsHelper = async ({
}) => { }) => {
let integrations; let integrations;
try { try {
integrations = await Integration.find({ integrations = await Integration.find({
workspace: workspaceId, workspace: workspaceId,
isActive: true, isActive: true,
app: { $ne: null } app: { $ne: null }
}).populate<{integrationAuth: IIntegrationAuth}>('integrationAuth', 'accessToken'); });
// for each workspace integration, sync/push secrets // for each workspace integration, sync/push secrets
// to that integration // to that integration
@@ -139,22 +143,23 @@ const syncIntegrationsHelper = async ({
environment: integration.environment environment: integration.environment
}); });
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
if (!integrationAuth) throw new Error('Failed to find integration auth');
// get integration auth access token // get integration auth access token
const accessToken = await getIntegrationAuthAccessHelper({ const accessToken = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth._id.toString() integrationAuthId: integration.integrationAuth.toString()
}); });
// sync secrets to integration // sync secrets to integration
await syncSecrets({ await syncSecrets({
integration: integration.integration, integration,
app: integration.app, integrationAuth,
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');
+61 -8
View File
@@ -1,10 +1,15 @@
import axios from 'axios'; import axios from 'axios';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import {
IIntegrationAuth
} from '../models';
import { import {
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_HEROKU_API_URL, INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL
} from '../variables'; } from '../variables';
/** /**
@@ -16,15 +21,21 @@ import {
* @returns {String} apps.name - name of integration app * @returns {String} apps.name - name of integration app
*/ */
const getApps = async ({ const getApps = async ({
integration, integrationAuth,
accessToken accessToken
}: { }: {
integration: string; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
}) => { }) => {
let apps;
interface App {
name: string;
siteId?: string;
}
let apps: App[]; // TODO: add type and define payloads for apps
try { try {
switch (integration) { switch (integrationAuth.integration) {
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
apps = await getAppsHeroku({ apps = await getAppsHeroku({
accessToken accessToken
@@ -35,6 +46,12 @@ const getApps = async ({
accessToken accessToken
}); });
break; break;
case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({
integrationAuth,
accessToken
});
break;
} }
} catch (err) { } catch (err) {
@@ -82,9 +99,9 @@ const getAppsHeroku = async ({
/** /**
* Return list of names of apps for Vercel integration * Return list of names of apps for Vercel integration
* @param {Object} obj * @param {Object} obj
* @param {String} obj.accessToken - access token for Heroku API * @param {String} obj.accessToken - access token for Vercel API
* @returns {Object[]} apps - names of Heroku apps * @returns {Object[]} apps - names of Vercel apps
* @returns {String} apps.name - name of Heroku app * @returns {String} apps.name - name of Vercel app
*/ */
const getAppsVercel = async ({ const getAppsVercel = async ({
accessToken accessToken
@@ -111,6 +128,42 @@ const getAppsVercel = async ({
return apps; 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 { export {
getApps getApps
} }
+36 -21
View File
@@ -35,6 +35,14 @@ interface ExchangeCodeVercelResponse {
team_id?: string; 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 * Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
* code-token exchange for integration named [integration] * code-token exchange for integration named [integration]
@@ -152,7 +160,6 @@ const exchangeCodeVercel = async ({
redirect_uri: `${SITE_URL}/vercel` redirect_uri: `${SITE_URL}/vercel`
} as any) } as any)
)).data; )).data;
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@@ -182,42 +189,50 @@ const exchangeCodeNetlify = async ({
}: { }: {
code: string; code: string;
}) => { }) => {
console.log('exchangeCodeNetlify'); let res: ExchangeCodeNetlifyResponse;
let res: ExchangeCodeVercelResponse; let accountId;
try { try {
res = (await axios.post( res = (await axios.post(
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
grant_type: 'authorization_code',
code: code, code: code,
client_id: CLIENT_ID_VERCEL, client_id: CLIENT_ID_NETLIFY,
client_secret: CLIENT_SECRET_VERCEL, client_secret: CLIENT_SECRET_NETLIFY,
redirect_uri: `${SITE_URL}/vercel` redirect_uri: `${SITE_URL}/netlify`
} as any) } as any)
)).data; )).data;
res = (await axios.post( const res2 = await axios.get(
INTEGRATION_NETLIFY_TOKEN_URL, 'https://api.netlify.com/api/v1/sites',
`${"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, headers: {
// new URLSearchParams({ Authorization: `Bearer ${res.access_token}`
// code: code, }
// client_id: CLIENT_ID_NETLIFY, }
// client_secret: CLIENT_SECRET_NETLIFY, );
// redirect_uri: `${SITE_URL}/netlify`
// } as any)
));
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) { } catch (err) {
console.error('netlify err', err);
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Netlify'); throw new Error('Failed OAuth2 code-token exchange with Netlify');
} }
return ({ return ({
accessToken: res.access_token,
refreshToken: res.refresh_token,
accountId
}); });
} }
+197 -31
View File
@@ -1,10 +1,15 @@
import axios from 'axios'; import axios from 'axios';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import {
IIntegration, IIntegrationAuth
} from '../models';
import { import {
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_HEROKU_API_URL, INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL
} from '../variables'; } from '../variables';
// TODO: need a helper function in the future to handle integration // 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] * Sync/push [secrets] to [app] in integration named [integration]
* @param {Object} obj * @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.app - app in integration
* @param {Object} obj.target - (optional) target (environment) 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)
@@ -21,33 +27,39 @@ import {
*/ */
const syncSecrets = async ({ const syncSecrets = async ({
integration, integration,
app, integrationAuth,
target,
secrets, secrets,
accessToken, accessToken,
}: { }: {
integration: string; integration: IIntegration;
app: string; integrationAuth: IIntegrationAuth;
target: string;
secrets: any; secrets: any;
accessToken: string; accessToken: string;
}) => { }) => {
try { try {
switch (integration) { switch (integration.integration) {
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
await syncSecretsHeroku({ await syncSecretsHeroku({
app, integration,
secrets, secrets,
accessToken accessToken
}); });
break; break;
case INTEGRATION_VERCEL: case INTEGRATION_VERCEL:
await syncSecretsVercel({ await syncSecretsVercel({
app, integration,
target,
secrets, secrets,
accessToken accessToken
}); });
break;
case INTEGRATION_NETLIFY:
await syncSecretsNetlify({
integration,
integrationAuth,
secrets,
accessToken
});
break;
} }
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -59,21 +71,21 @@ const syncSecrets = async ({
/** /**
* Sync/push [secrets] to Heroku [app] * Sync/push [secrets] to Heroku [app]
* @param {Object} obj * @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) * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
*/ */
const syncSecretsHeroku = async ({ const syncSecretsHeroku = async ({
app, integration,
secrets, secrets,
accessToken accessToken
}: { }: {
app: string; integration: IIntegration,
secrets: any; secrets: any;
accessToken: string; accessToken: string;
}) => { }) => {
try { try {
const herokuSecrets = (await axios.get( const herokuSecrets = (await axios.get(
`${INTEGRATION_HEROKU_API_URL}/apps/${app}/config-vars`, `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
{ {
headers: { headers: {
Accept: 'application/vnd.heroku+json; version=3', Accept: 'application/vnd.heroku+json; version=3',
@@ -89,7 +101,7 @@ const syncSecretsHeroku = async ({
}); });
await axios.patch( await axios.patch(
`${INTEGRATION_HEROKU_API_URL}/apps/${app}/config-vars`, `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
secrets, secrets,
{ {
headers: { headers: {
@@ -108,18 +120,15 @@ const syncSecretsHeroku = async ({
/** /**
* Sync/push [secrets] to Heroku [app] * Sync/push [secrets] to Heroku [app]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.app - app in integration * @param {IIntegration} obj.integration - integration details
* @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) * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
*/ */
const syncSecretsVercel = async ({ const syncSecretsVercel = async ({
app, integration,
target,
secrets, secrets,
accessToken accessToken
}: { }: {
app: string; integration: IIntegration,
target: string;
secrets: any; secrets: any;
accessToken: string; accessToken: string;
}) => { }) => {
@@ -140,7 +149,7 @@ const syncSecretsVercel = async ({
}); });
const res = (await Promise.all((await axios.get( 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, params,
headers: { headers: {
@@ -150,9 +159,9 @@ const syncSecretsVercel = async ({
)) ))
.data .data
.envs .envs
.filter((secret: VercelSecret) => secret.target.includes(target)) .filter((secret: VercelSecret) => secret.target.includes(integration.target))
.map(async (secret: VercelSecret) => (await axios.get( .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: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`
@@ -172,11 +181,12 @@ const syncSecretsVercel = async ({
// Identify secrets to create // Identify secrets to create
Object.keys(secrets).map((key) => { Object.keys(secrets).map((key) => {
if (!(key in res)) { if (!(key in res)) {
// case: secret has been created
newSecrets.push({ newSecrets.push({
key: key, key: key,
value: secrets[key], value: secrets[key],
type: 'encrypted', type: 'encrypted',
target: [target] target: [integration.target]
}); });
} }
}); });
@@ -191,7 +201,7 @@ const syncSecretsVercel = async ({
key: key, key: key,
value: secrets[key], value: secrets[key],
type: 'encrypted', type: 'encrypted',
target: [target] target: [integration.target]
}); });
} }
} else { } else {
@@ -201,7 +211,7 @@ const syncSecretsVercel = async ({
key: key, key: key,
value: res[key].value, value: res[key].value,
type: 'encrypted', type: 'encrypted',
target: [target], target: [integration.target],
}); });
} }
}); });
@@ -209,7 +219,7 @@ const syncSecretsVercel = async ({
// Sync/push new secrets // Sync/push new secrets
if (newSecrets.length > 0) { if (newSecrets.length > 0) {
await axios.post( await axios.post(
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${app}/env`, `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
newSecrets, newSecrets,
{ {
headers: { headers: {
@@ -227,7 +237,7 @@ const syncSecretsVercel = async ({
...updatedSecret ...updatedSecret
} = secret; } = secret;
await axios.patch( 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, updatedSecret,
{ {
headers: { headers: {
@@ -242,7 +252,7 @@ const syncSecretsVercel = async ({
if (deleteSecrets.length > 0) { if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (secret: VercelSecret) => { deleteSecrets.forEach(async (secret: VercelSecret) => {
await axios.delete( 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: { headers: {
Authorization: `Bearer ${accessToken}` 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 { export {
syncSecrets syncSecrets
} }
+11 -1
View File
@@ -16,7 +16,9 @@ export interface IIntegration {
isActive: boolean; isActive: boolean;
app: string; app: string;
target: string; target: string;
integration: 'heroku' | 'netlify'; context: string;
siteId: string;
integration: 'heroku' | 'vercel' | 'netlify';
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
@@ -44,6 +46,14 @@ const integrationSchema = new Schema<IIntegration>(
type: String, type: String,
default: null default: null
}, },
context: { // netlify-specific context (deploy)
type: String,
default: null
},
siteId: { // netlify-specific site (app) id
type: String,
default: null
},
integration: { integration: {
type: String, type: String,
enum: [ enum: [
+6 -2
View File
@@ -8,8 +8,9 @@ import {
export interface IIntegrationAuth { export interface IIntegrationAuth {
_id: Types.ObjectId; _id: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
integration: 'heroku' | 'netlify'; integration: 'heroku' | 'vercel' | 'netlify';
teamId: string; teamId: string;
accountId: string;
refreshCiphertext?: string; refreshCiphertext?: string;
refreshIV?: string; refreshIV?: string;
refreshTag?: string; refreshTag?: string;
@@ -34,7 +35,10 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
], ],
required: true required: true
}, },
teamId: { // vercel-specific integration param set at OAuth2 code-token exchange teamId: { // vercel-specific integration param
type: String
},
accountId: { // netlify-specific integration param
type: String type: String
}, },
refreshCiphertext: { refreshCiphertext: {
+3
View File
@@ -20,6 +20,9 @@ router.patch(
body('app').exists().trim(), body('app').exists().trim(),
body('environment').exists().trim(), body('environment').exists().trim(),
body('isActive').exists().isBoolean(), body('isActive').exists().isBoolean(),
body('target').exists(),
body('context').exists(),
body('siteId').exists(),
validateRequest, validateRequest,
integrationController.updateIntegration integrationController.updateIntegration
); );
@@ -8,7 +8,8 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
envMapping, envMapping,
reverseEnvMapping reverseEnvMapping,
reverseContextNetlifyMapping
} 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"
@@ -36,23 +37,85 @@ const Integration = ({
); );
const [fileState, setFileState] = useState([]); const [fileState, setFileState] = useState([]);
const router = useRouter(); const router = useRouter();
const [apps, setApps] = useState([]); const [apps, setApps] = useState([]); // integration app objects
const [integrationApp, setIntegrationApp] = useState(null); const [integrationApp, setIntegrationApp] = useState(null); // integration app name
const [integrationTarget, setIntegrationTarget] = useState(null); const [integrationTarget, setIntegrationTarget] = useState(null); // vercel-specific integration param
const [integrationContext, setIntegrationContext] = useState(null); // netlify-specific integration param
useEffect(async () => { useEffect(async () => {
interface App {
name: string;
siteId?: string;
}
const tempApps = await getIntegrationApps({ const tempApps = await getIntegrationApps({
integrationAuthId: integration.integrationAuth, integrationAuthId: integration.integrationAuth,
}); });
const tempAppNames = tempApps.map((app) => app.name); setApps(tempApps);
setApps(tempAppNames);
setIntegrationApp( setIntegrationApp(
integration.app ? integration.app : tempAppNames[0] integration.app ? integration.app : tempApps[0].name
); );
setIntegrationTarget("Development");
switch (integration.integration) {
case "vercel":
setIntegrationTarget("Development");
break;
case "netlify":
setIntegrationContext("All");
break;
default:
break;
}
}, []); }, []);
const renderIntegrationSpecificParams = (integration) => {
try {
switch (integration.integration) {
case "vercel":
return (
<div>
<div className="text-gray-400 text-xs font-semibold mb-2">
ENVIRONMENT
</div>
<ListBox
data={!integration.isActive && [
"Production",
"Preview",
"Development"
]}
selected={"Production"}
onChange={setIntegrationTarget}
/>
</div>
);
case "netlify":
return (
<div>
<div className="text-gray-400 text-xs font-semibold mb-2">
CONTEXT
</div>
<ListBox
data={!integration.isActive && [
"All",
"Production",
"Deploy previews",
"Branch deploys",
"Local development"
]}
selected={integrationContext}
onChange={setIntegrationContext}
/>
</div>
);
default:
return <div></div>;
}
} catch (err) {
console.error(err);
}
}
if (!integrationApp || apps.length === 0) return <div></div> if (!integrationApp || apps.length === 0) return <div></div>
return ( return (
@@ -91,27 +154,12 @@ const Integration = ({
APP APP
</div> </div>
<ListBox <ListBox
data={!integration.isActive && apps} data={!integration.isActive && apps.map((app) => app.name)}
selected={integrationApp} selected={integrationApp}
onChange={setIntegrationApp} onChange={setIntegrationApp}
/> />
</div> </div>
{integration.integration === "vercel" && ( {renderIntegrationSpecificParams(integration)}
<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>
<div className="flex items-end"> <div className="flex items-end">
{integration.isActive ? ( {integration.isActive ? (
@@ -131,7 +179,9 @@ const Integration = ({
environment: envMapping[integrationEnvironment], environment: envMapping[integrationEnvironment],
app: integrationApp, app: integrationApp,
isActive: true, isActive: true,
target: integrationTarget.toLowerCase() target: integrationTarget ? integrationTarget.toLowerCase() : null,
context: integrationContext ? reverseContextNetlifyMapping[integrationContext] : null,
siteId: apps.find((app) => app.name === integrationApp).siteId
}); });
router.reload(); router.reload();
}} }}
@@ -9,7 +9,9 @@ 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) * @param {String} obj.target - (optional) target (environment) for Vercel integration
* @param {String} obj.context - (optional) context (environment) for Netlify integration
* @param {String} obj.siteId - (optional) app (site_id) for Netlify integration
* @returns * @returns
*/ */
const updateIntegration = ({ const updateIntegration = ({
@@ -17,7 +19,9 @@ const updateIntegration = ({
app, app,
environment, environment,
isActive, isActive,
target target,
context,
siteId
}) => { }) => {
return SecurityClient.fetchCall( return SecurityClient.fetchCall(
"/api/v1/integration/" + integrationId, "/api/v1/integration/" + integrationId,
@@ -30,7 +34,9 @@ const updateIntegration = ({
app, app,
environment, environment,
isActive, isActive,
target target,
context,
siteId
}), }),
} }
).then(async (res) => { ).then(async (res) => {
-2
View File
@@ -16,11 +16,9 @@ export default function Netlify() {
*/ */
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(async () => { useEffect(async () => {
console.log('AA');
if (state === localStorage.getItem('latestCSRFToken')) { if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken'); localStorage.removeItem('latestCSRFToken');
console.log('Netlify', parsedUrl);
// http://localhost:8080/netlify?code=qnG0g_krhklWDpdUqfhU-t1sLeZzYI3gF2d6QVnL-Gc&state=3a78cd3154a9a99ddd4eb5d99dbb3289 // http://localhost:8080/netlify?code=qnG0g_krhklWDpdUqfhU-t1sLeZzYI3gF2d6QVnL-Gc&state=3a78cd3154a9a99ddd4eb5d99dbb3289
// http://localhost:8080/netlify#access_token=d-78qUAnnSzvlgfG9Y_oUV6_4TQBxLbofImiBbKAjzE&token_type=Bearer&state=5da34fa49e301e9fa1a6e40925694b77 // http://localhost:8080/netlify#access_token=d-78qUAnnSzvlgfG9Y_oUV6_4TQBxLbofImiBbKAjzE&token_type=Bearer&state=5da34fa49e301e9fa1a6e40925694b77
+14 -1
View File
@@ -12,7 +12,20 @@ const reverseEnvMapping = {
test: "Testing", test: "Testing",
}; };
const vercelMapping = {
}
const reverseContextNetlifyMapping = {
"All": "all",
"Local development": "dev",
"Branch deploys": "branch-deploy",
"Deploy Previews": "deploy-preview",
"Production": "production"
}
export { export {
envMapping, envMapping,
reverseEnvMapping reverseEnvMapping,
reverseContextNetlifyMapping
}; };