mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 21:28:27 +00:00
Finish Netlify integration v1 full-loop
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user