mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 07:28:51 +00:00
Add Render, Fly.io integrations and reduce integrations page reloads
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import axios from 'axios';
|
||||
import { readFileSync } from 'fs';
|
||||
import { IntegrationAuth, Integration } from '../../models';
|
||||
import {
|
||||
Integration,
|
||||
IntegrationAuth,
|
||||
Bot
|
||||
} from '../../models';
|
||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
|
||||
import { IntegrationService } from '../../services';
|
||||
import { getApps, revokeAccess } from '../../integrations';
|
||||
@@ -44,7 +47,7 @@ export const oAuthExchange = async (
|
||||
environment: environments[0].slug,
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get OAuth2 code-token exchange'
|
||||
@@ -56,6 +59,67 @@ export const oAuthExchange = async (
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Save integration access token as part of integration [integration] for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const saveIntegrationAccessToken = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
// TODO: refactor
|
||||
let integrationAuth;
|
||||
try {
|
||||
const {
|
||||
workspaceId,
|
||||
accessToken,
|
||||
integration
|
||||
}: {
|
||||
workspaceId: string;
|
||||
accessToken: string;
|
||||
integration: string;
|
||||
} = req.body;
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
integration
|
||||
}, {
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
integration
|
||||
}, {
|
||||
new: true,
|
||||
upsert: true
|
||||
});
|
||||
|
||||
const bot = await Bot.findOne({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!bot) throw new Error('Bot must be enabled to save integration access token');
|
||||
|
||||
// encrypt and save integration access token
|
||||
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
accessToken,
|
||||
accessExpiresAt: undefined
|
||||
});
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to save integration access token');
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to save access token for integration'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
integrationAuth
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of applications allowed for integration with integration authorization id [integrationAuthId]
|
||||
* @param req
|
||||
@@ -70,7 +134,7 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||
accessToken: req.accessToken
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get integration authorization applications'
|
||||
@@ -89,15 +153,14 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
||||
let integrationAuth;
|
||||
try {
|
||||
const { integrationAuthId } = req.params;
|
||||
|
||||
await revokeAccess({
|
||||
integrationAuth = await revokeAccess({
|
||||
integrationAuth: req.integrationAuth,
|
||||
accessToken: req.accessToken
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete integration authorization'
|
||||
@@ -105,6 +168,6 @@ export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully deleted integration authorization'
|
||||
integrationAuth
|
||||
});
|
||||
}
|
||||
@@ -1,25 +1,43 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { readFileSync } from 'fs';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Integration, Bot, BotKey } from '../../models';
|
||||
import {
|
||||
Integration,
|
||||
Workspace,
|
||||
Bot,
|
||||
BotKey
|
||||
} from '../../models';
|
||||
import { EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
|
||||
interface Key {
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
}
|
||||
/**
|
||||
* Create/initialize an (empty) integration for integration authorization
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createIntegration = async (req: Request, res: Response) => {
|
||||
let integration;
|
||||
try {
|
||||
// initialize new integration after saving integration access token
|
||||
integration = await new Integration({
|
||||
workspace: req.integrationAuth.workspace._id,
|
||||
isActive: false,
|
||||
app: null,
|
||||
environment: req.integrationAuth.workspace?.environments[0].slug,
|
||||
integration: req.integrationAuth.integration,
|
||||
integrationAuth: req.integrationAuth._id
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to create integration'
|
||||
});
|
||||
}
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
ivKey: string;
|
||||
tagKey: string;
|
||||
hashKey: string;
|
||||
ciphertextValue: string;
|
||||
ivValue: string;
|
||||
tagValue: string;
|
||||
hashValue: string;
|
||||
type: 'shared' | 'personal';
|
||||
return res.status(200).send({
|
||||
integration
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,13 +54,12 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
||||
|
||||
try {
|
||||
const {
|
||||
app,
|
||||
environment,
|
||||
isActive,
|
||||
target, // vercel-specific integration param
|
||||
context, // netlify-specific integration param
|
||||
siteId, // netlify-specific integration param
|
||||
owner // github-specific integration param
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner, // github-specific integration param
|
||||
} = req.body;
|
||||
|
||||
integration = await Integration.findOneAndUpdate(
|
||||
@@ -53,9 +70,8 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
||||
environment,
|
||||
isActive,
|
||||
app,
|
||||
target,
|
||||
context,
|
||||
siteId,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner
|
||||
},
|
||||
{
|
||||
@@ -101,27 +117,6 @@ export const deleteIntegration = async (req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
if (!integration) throw new Error('Failed to find integration');
|
||||
|
||||
const integrations = await Integration.find({
|
||||
workspace: integration.workspace
|
||||
});
|
||||
|
||||
if (integrations.length === 0) {
|
||||
// case: no integrations left, deactivate bot
|
||||
const bot = await Bot.findOneAndUpdate({
|
||||
workspace: integration.workspace
|
||||
}, {
|
||||
isActive: false
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
|
||||
if (bot) {
|
||||
await BotKey.deleteOne({
|
||||
bot: bot._id
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
|
||||
@@ -127,7 +127,6 @@ const syncIntegrationsHelper = async ({
|
||||
}) => {
|
||||
let integrations;
|
||||
try {
|
||||
|
||||
integrations = await Integration.find({
|
||||
workspace: workspaceId,
|
||||
isActive: true,
|
||||
@@ -142,7 +141,7 @@ const syncIntegrationsHelper = async ({
|
||||
workspaceId: integration.workspace.toString(),
|
||||
environment: integration.environment
|
||||
});
|
||||
|
||||
|
||||
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
@@ -316,7 +315,7 @@ const setIntegrationAuthAccessHelper = async ({
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
accessToken: string;
|
||||
accessExpiresAt: Date;
|
||||
accessExpiresAt: Date | undefined;
|
||||
}) => {
|
||||
let integrationAuth;
|
||||
try {
|
||||
|
||||
@@ -7,9 +7,13 @@ import {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
INTEGRATION_FLYIO_API_URL
|
||||
} from '../variables';
|
||||
|
||||
/**
|
||||
@@ -29,10 +33,11 @@ const getApps = async ({
|
||||
}) => {
|
||||
interface App {
|
||||
name: string;
|
||||
siteId?: string;
|
||||
appId?: string;
|
||||
owner?: string;
|
||||
}
|
||||
|
||||
let apps: App[]; // TODO: add type and define payloads for apps
|
||||
let apps: App[];
|
||||
try {
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_HEROKU:
|
||||
@@ -48,13 +53,21 @@ const getApps = async ({
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
apps = await getAppsNetlify({
|
||||
integrationAuth,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_GITHUB:
|
||||
apps = await getAppsGithub({
|
||||
integrationAuth,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_RENDER:
|
||||
apps = await getAppsRender({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_FLYIO:
|
||||
apps = await getAppsFlyio({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
@@ -69,7 +82,7 @@ const getApps = async ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of names of apps for Heroku integration
|
||||
* Return list of apps for Heroku integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - access token for Heroku API
|
||||
* @returns {Object[]} apps - names of Heroku apps
|
||||
@@ -141,17 +154,15 @@ const getAppsVercel = async ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of names of sites for Netlify integration
|
||||
* Return list 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;
|
||||
@@ -166,7 +177,7 @@ const getAppsNetlify = async ({
|
||||
|
||||
apps = res.map((a: any) => ({
|
||||
name: a.name,
|
||||
siteId: a.site_id
|
||||
appId: a.site_id
|
||||
}));
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
@@ -178,17 +189,15 @@ const getAppsNetlify = async ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of names of repositories for Github integration
|
||||
* Return list of repositories for Github 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 getAppsGithub = async ({
|
||||
integrationAuth,
|
||||
accessToken
|
||||
}: {
|
||||
integrationAuth: IIntegrationAuth;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let apps;
|
||||
@@ -220,4 +229,94 @@ const getAppsGithub = async ({
|
||||
return apps;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of services for Render integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - access token for Render API
|
||||
* @returns {Object[]} apps - names and ids of Render services
|
||||
* @returns {String} apps.name - name of Render service
|
||||
* @returns {String} apps.appId - id of Render service
|
||||
*/
|
||||
const getAppsRender = async ({
|
||||
accessToken
|
||||
}: {
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let apps: any;
|
||||
try {
|
||||
const res = (
|
||||
await axios.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
})
|
||||
).data;
|
||||
|
||||
apps = res
|
||||
.map((a: any) => ({
|
||||
name: a.service.name,
|
||||
appId: a.service.id
|
||||
}));
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get Render services');
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of apps for Fly.io integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - access token for Fly.io API
|
||||
* @returns {Object[]} apps - names and ids of Fly.io apps
|
||||
* @returns {String} apps.name - name of Fly.io apps
|
||||
*/
|
||||
const getAppsFlyio = async ({
|
||||
accessToken
|
||||
}: {
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let apps;
|
||||
try {
|
||||
const query = `
|
||||
query($role: String) {
|
||||
apps(type: "container", first: 400, role: $role) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
hostname
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const res = (await axios({
|
||||
url: INTEGRATION_FLYIO_API_URL,
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken
|
||||
},
|
||||
data: {
|
||||
query,
|
||||
variables: {
|
||||
role: null
|
||||
}
|
||||
}
|
||||
})).data.data.apps.nodes;
|
||||
|
||||
apps = res
|
||||
.map((a: any) => ({
|
||||
name: a.name
|
||||
}));
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get Fly.io apps');
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
export { getApps };
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { IIntegrationAuth, IntegrationAuth, Integration } from '../models';
|
||||
import {
|
||||
IIntegrationAuth,
|
||||
IntegrationAuth,
|
||||
Integration,
|
||||
Bot,
|
||||
BotKey
|
||||
} from '../models';
|
||||
import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
@@ -15,6 +20,7 @@ const revokeAccess = async ({
|
||||
integrationAuth: IIntegrationAuth;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let deletedIntegrationAuth;
|
||||
try {
|
||||
// add any integration-specific revocation logic
|
||||
switch (integrationAuth.integration) {
|
||||
@@ -28,7 +34,7 @@ const revokeAccess = async ({
|
||||
break;
|
||||
}
|
||||
|
||||
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||
_id: integrationAuth._id
|
||||
});
|
||||
|
||||
@@ -42,6 +48,8 @@ const revokeAccess = async ({
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to delete integration authorization');
|
||||
}
|
||||
|
||||
return deletedIntegrationAuth;
|
||||
};
|
||||
|
||||
export { revokeAccess };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
// import * as sodium from 'libsodium-wrappers';
|
||||
@@ -10,9 +10,13 @@ import {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
INTEGRATION_FLYIO_API_URL
|
||||
} from '../variables';
|
||||
import { access, appendFile } from 'fs';
|
||||
|
||||
@@ -21,8 +25,6 @@ import { access, appendFile } from 'fs';
|
||||
* @param {Object} obj
|
||||
* @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)
|
||||
* @param {String} obj.accessToken - access token for integration
|
||||
*/
|
||||
@@ -69,6 +71,20 @@ const syncSecrets = async ({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_RENDER:
|
||||
await syncSecretsRender({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_FLYIO:
|
||||
await syncSecretsFlyio({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
@@ -78,10 +94,11 @@ const syncSecrets = async ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Heroku [app]
|
||||
* Sync/push [secrets] to Heroku app named [integration.app]
|
||||
* @param {Object} obj
|
||||
* @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 {String} obj.accessToken - access token for Heroku integration
|
||||
*/
|
||||
const syncSecretsHeroku = async ({
|
||||
integration,
|
||||
@@ -129,7 +146,7 @@ const syncSecretsHeroku = async ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Heroku [app]
|
||||
* Sync/push [secrets] to Vercel project named [integration.app]
|
||||
* @param {Object} obj
|
||||
* @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)
|
||||
@@ -174,7 +191,7 @@ const syncSecretsVercel = async ({
|
||||
))
|
||||
.data
|
||||
.envs
|
||||
.filter((secret: VercelSecret) => secret.target.includes(integration.target))
|
||||
.filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment))
|
||||
.map(async (secret: VercelSecret) => (await axios.get(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
{
|
||||
@@ -201,7 +218,7 @@ const syncSecretsVercel = async ({
|
||||
key: key,
|
||||
value: secrets[key],
|
||||
type: 'encrypted',
|
||||
target: [integration.target]
|
||||
target: [integration.targetEnvironment]
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -216,7 +233,7 @@ const syncSecretsVercel = async ({
|
||||
key: key,
|
||||
value: secrets[key],
|
||||
type: 'encrypted',
|
||||
target: [integration.target]
|
||||
target: [integration.targetEnvironment]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -226,7 +243,7 @@ const syncSecretsVercel = async ({
|
||||
key: key,
|
||||
value: res[key].value,
|
||||
type: 'encrypted',
|
||||
target: [integration.target],
|
||||
target: [integration.targetEnvironment],
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -287,11 +304,12 @@ const syncSecretsVercel = async ({
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Netlify site [app]
|
||||
* Sync/push [secrets] to Netlify site with id [integration.appId]
|
||||
* @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)
|
||||
* @param {Object} obj.accessToken - access token for Netlify integration
|
||||
*/
|
||||
const syncSecretsNetlify = async ({
|
||||
integration,
|
||||
@@ -323,7 +341,7 @@ const syncSecretsNetlify = async ({
|
||||
|
||||
const getParams = new URLSearchParams({
|
||||
context_name: 'all', // integration.context or all
|
||||
site_id: integration.siteId
|
||||
site_id: integration.appId
|
||||
});
|
||||
|
||||
const res = (await axios.get(
|
||||
@@ -354,7 +372,7 @@ const syncSecretsNetlify = async ({
|
||||
key,
|
||||
values: [{
|
||||
value: secrets[key],
|
||||
context: integration.context
|
||||
context: integration.targetEnvironment
|
||||
}]
|
||||
});
|
||||
} else {
|
||||
@@ -365,15 +383,15 @@ const syncSecretsNetlify = async ({
|
||||
[value.context]: value
|
||||
}), {});
|
||||
|
||||
if (integration.context in contexts) {
|
||||
if (integration.targetEnvironment in contexts) {
|
||||
// case: Netlify secret value exists in integration context
|
||||
if (secrets[key] !== contexts[integration.context].value) {
|
||||
if (secrets[key] !== contexts[integration.targetEnvironment].value) {
|
||||
// case: Infisical and Netlify secret values are different
|
||||
// -> update Netlify secret context and value
|
||||
updateSecrets.push({
|
||||
key,
|
||||
values: [{
|
||||
context: integration.context,
|
||||
context: integration.targetEnvironment,
|
||||
value: secrets[key]
|
||||
}]
|
||||
});
|
||||
@@ -384,7 +402,7 @@ const syncSecretsNetlify = async ({
|
||||
updateSecrets.push({
|
||||
key,
|
||||
values: [{
|
||||
context: integration.context,
|
||||
context: integration.targetEnvironment,
|
||||
value: secrets[key]
|
||||
}]
|
||||
});
|
||||
@@ -402,7 +420,7 @@ const syncSecretsNetlify = async ({
|
||||
const numberOfValues = res[key].values.length;
|
||||
|
||||
res[key].values.forEach((value: NetlifyValue) => {
|
||||
if (value.context === integration.context) {
|
||||
if (value.context === integration.targetEnvironment) {
|
||||
if (numberOfValues <= 1) {
|
||||
// case: Netlify secret value has less than 1 context -> delete secret
|
||||
deleteSecrets.push(key);
|
||||
@@ -412,7 +430,7 @@ const syncSecretsNetlify = async ({
|
||||
key,
|
||||
values: [{
|
||||
id: value.id,
|
||||
context: integration.context,
|
||||
context: integration.targetEnvironment,
|
||||
value: value.value
|
||||
}]
|
||||
});
|
||||
@@ -423,7 +441,7 @@ const syncSecretsNetlify = async ({
|
||||
});
|
||||
|
||||
const syncParams = new URLSearchParams({
|
||||
site_id: integration.siteId
|
||||
site_id: integration.appId
|
||||
});
|
||||
|
||||
if (newSecrets.length > 0) {
|
||||
@@ -492,11 +510,12 @@ const syncSecretsNetlify = async ({
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to GitHub [repo]
|
||||
* Sync/push [secrets] to GitHub repo with name [integration.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)
|
||||
* @param {String} obj.accessToken - access token for GitHub integration
|
||||
*/
|
||||
const syncSecretsGitHub = async ({
|
||||
integration,
|
||||
@@ -605,4 +624,175 @@ const syncSecretsGitHub = async ({
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Render service with id [integration.appId]
|
||||
* @param {Object} obj
|
||||
* @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 {String} obj.accessToken - access token for Render integration
|
||||
*/
|
||||
const syncSecretsRender = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
secrets: any;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
await axios.put(
|
||||
`${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`,
|
||||
Object.keys(secrets).map((key) => ({
|
||||
key,
|
||||
value: secrets[key]
|
||||
})),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to Render');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Fly.io app
|
||||
* @param {Object} obj
|
||||
* @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 {String} obj.accessToken - access token for Render integration
|
||||
*/
|
||||
const syncSecretsFlyio = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
secrets: any;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
// set secrets
|
||||
const SetSecrets = `
|
||||
mutation($input: SetSecretsInput!) {
|
||||
setSecrets(input: $input) {
|
||||
release {
|
||||
id
|
||||
version
|
||||
reason
|
||||
description
|
||||
user {
|
||||
id
|
||||
email
|
||||
name
|
||||
}
|
||||
evaluationId
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
await axios({
|
||||
url: INTEGRATION_FLYIO_API_URL,
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken
|
||||
},
|
||||
data: {
|
||||
query: SetSecrets,
|
||||
variables: {
|
||||
input: {
|
||||
appId: integration.app,
|
||||
secrets: Object.entries(secrets).map(([key, value]) => ({ key, value }))
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// get secrets
|
||||
interface FlyioSecret {
|
||||
name: string;
|
||||
digest: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const GetSecrets = `query ($appName: String!) {
|
||||
app(name: $appName) {
|
||||
secrets {
|
||||
name
|
||||
digest
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const getSecretsRes = (await axios({
|
||||
method: 'post',
|
||||
url: INTEGRATION_FLYIO_API_URL,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
query: GetSecrets,
|
||||
variables: {
|
||||
appName: integration.app
|
||||
}
|
||||
}
|
||||
})).data.data.app.secrets;
|
||||
|
||||
const deleteSecretsKeys = getSecretsRes
|
||||
.filter((secret: FlyioSecret) => !(secret.name in secrets))
|
||||
.map((secret: FlyioSecret) => secret.name);
|
||||
|
||||
// unset (delete) secrets
|
||||
const DeleteSecrets = `mutation($input: UnsetSecretsInput!) {
|
||||
unsetSecrets(input: $input) {
|
||||
release {
|
||||
id
|
||||
version
|
||||
reason
|
||||
description
|
||||
user {
|
||||
id
|
||||
email
|
||||
name
|
||||
}
|
||||
evaluationId
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: INTEGRATION_FLYIO_API_URL,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
query: DeleteSecrets,
|
||||
variables: {
|
||||
input: {
|
||||
appId: integration.app,
|
||||
keys: deleteSecretsKeys
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to Fly.io');
|
||||
}
|
||||
}
|
||||
|
||||
export { syncSecrets };
|
||||
@@ -1,10 +1,12 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { IntegrationAuth } from '../models';
|
||||
import { IntegrationAuth, IWorkspace } from '../models';
|
||||
import { IntegrationService } from '../services';
|
||||
import { validateMembership } from '../helpers/membership';
|
||||
import { UnauthorizedRequestError } from '../utils/errors';
|
||||
|
||||
type req = 'params' | 'body' | 'query';
|
||||
|
||||
/**
|
||||
* Validate if user on request is a member of workspace with proper roles associated
|
||||
* with the integration authorization on request params.
|
||||
@@ -14,17 +16,21 @@ import { UnauthorizedRequestError } from '../utils/errors';
|
||||
*/
|
||||
const requireIntegrationAuthorizationAuth = ({
|
||||
acceptedRoles,
|
||||
attachAccessToken = true
|
||||
attachAccessToken = true,
|
||||
location = 'params'
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
attachAccessToken?: boolean;
|
||||
location?: req;
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { integrationAuthId } = req.params;
|
||||
const { integrationAuthId } = req[location];
|
||||
|
||||
const integrationAuth = await IntegrationAuth.findOne({
|
||||
_id: integrationAuthId
|
||||
}).select(
|
||||
})
|
||||
.populate<{ workspace: IWorkspace }>('workspace')
|
||||
.select(
|
||||
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
|
||||
);
|
||||
|
||||
@@ -34,7 +40,7 @@ const requireIntegrationAuthorizationAuth = ({
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
workspaceId: integrationAuth.workspace._id.toString(),
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO
|
||||
} from '../variables';
|
||||
|
||||
export interface IIntegration {
|
||||
@@ -12,20 +14,19 @@ export interface IIntegration {
|
||||
environment: string;
|
||||
isActive: boolean;
|
||||
app: string;
|
||||
target: string;
|
||||
context: string;
|
||||
siteId: string;
|
||||
owner: string;
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github';
|
||||
targetEnvironment: string;
|
||||
appId: string;
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
|
||||
integrationAuth: Types.ObjectId;
|
||||
}
|
||||
|
||||
const integrationSchema = new Schema<IIntegration>(
|
||||
{
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
@@ -40,18 +41,13 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
target: {
|
||||
// vercel-specific target (environment)
|
||||
appId: { // (new)
|
||||
// id of app in provider
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
context: {
|
||||
// netlify-specific context (deploy)
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
siteId: {
|
||||
// netlify-specific site (app) id
|
||||
targetEnvironment: { // (new)
|
||||
// target environment
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
@@ -66,7 +62,9 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO
|
||||
],
|
||||
required: true
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
export interface IIntegrationAuth {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github';
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
|
||||
teamId: string;
|
||||
accountId: string;
|
||||
refreshCiphertext?: string;
|
||||
@@ -24,8 +24,9 @@ export interface IIntegrationAuth {
|
||||
const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
||||
{
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
required: true
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
integration: {
|
||||
type: String,
|
||||
|
||||
@@ -3,12 +3,27 @@ const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireIntegrationAuth,
|
||||
requireIntegrationAuthorizationAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER } from '../../variables';
|
||||
import { body, param } from 'express-validator';
|
||||
import { integrationController } from '../../controllers/v1';
|
||||
|
||||
router.post( // new: add new integration
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
}),
|
||||
body('integrationAuthId').exists().trim(),
|
||||
validateRequest,
|
||||
integrationController.createIntegration
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:integrationId',
|
||||
requireAuth({
|
||||
@@ -18,12 +33,11 @@ router.patch(
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('integrationId').exists().trim(),
|
||||
body('isActive').exists().isBoolean(),
|
||||
body('app').exists().trim(),
|
||||
body('environment').exists().trim(),
|
||||
body('isActive').exists().isBoolean(),
|
||||
body('target').exists(),
|
||||
body('context').exists(),
|
||||
body('siteId').exists(),
|
||||
body('appId').exists(),
|
||||
body('targetEnvironment').exists(),
|
||||
body('owner').exists(),
|
||||
validateRequest,
|
||||
integrationController.updateIntegration
|
||||
|
||||
@@ -34,6 +34,22 @@ router.post(
|
||||
integrationAuthController.oAuthExchange
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/access-token',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
}),
|
||||
body('workspaceId').exists().trim().notEmpty(),
|
||||
body('accessToken').exists().trim().notEmpty(),
|
||||
body('integration').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
integrationAuthController.saveIntegrationAccessToken
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:integrationAuthId/apps',
|
||||
requireAuth({
|
||||
|
||||
@@ -122,7 +122,7 @@ class IntegrationService {
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} obj.accessToken - access token
|
||||
* @param {String} obj.accessExpiresAt - expiration date of access token
|
||||
* @param {Date} obj.accessExpiresAt - expiration date of access token
|
||||
* @returns {IntegrationAuth} - updated integration auth
|
||||
*/
|
||||
static async setIntegrationAuthAccess({
|
||||
@@ -132,7 +132,7 @@ class IntegrationService {
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
accessToken: string;
|
||||
accessExpiresAt: Date;
|
||||
accessExpiresAt: Date | undefined;
|
||||
}) {
|
||||
return await setIntegrationAuthAccessHelper({
|
||||
integrationAuthId,
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
@@ -19,6 +21,8 @@ import {
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
INTEGRATION_FLYIO_API_URL,
|
||||
INTEGRATION_OPTIONS
|
||||
} from './integration';
|
||||
import {
|
||||
@@ -56,6 +60,8 @@ export {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
@@ -65,6 +71,8 @@ export {
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
INTEGRATION_FLYIO_API_URL,
|
||||
EVENT_PUSH_SECRETS,
|
||||
EVENT_PULL_SECRETS,
|
||||
ACTION_ADD_SECRETS,
|
||||
|
||||
@@ -10,11 +10,15 @@ const INTEGRATION_HEROKU = 'heroku';
|
||||
const INTEGRATION_VERCEL = 'vercel';
|
||||
const INTEGRATION_NETLIFY = 'netlify';
|
||||
const INTEGRATION_GITHUB = 'github';
|
||||
const INTEGRATION_RENDER = 'render';
|
||||
const INTEGRATION_FLYIO = 'flyio';
|
||||
const INTEGRATION_SET = new Set([
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO
|
||||
]);
|
||||
|
||||
// integration types
|
||||
@@ -32,23 +36,25 @@ const INTEGRATION_GITHUB_TOKEN_URL =
|
||||
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
|
||||
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
|
||||
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com';
|
||||
const INTEGRATION_RENDER_API_URL = 'https://api.render.com';
|
||||
const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql';
|
||||
|
||||
const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Heroku',
|
||||
slug: 'heroku',
|
||||
image: 'Heroku',
|
||||
image: 'Heroku.png',
|
||||
isAvailable: true,
|
||||
type: 'oauth2',
|
||||
type: 'oauth',
|
||||
clientId: CLIENT_ID_HEROKU,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Vercel',
|
||||
slug: 'vercel',
|
||||
image: 'Vercel',
|
||||
image: 'Vercel.png',
|
||||
isAvailable: true,
|
||||
type: 'vercel',
|
||||
type: 'oauth',
|
||||
clientId: '',
|
||||
clientSlug: CLIENT_SLUG_VERCEL,
|
||||
docsLink: ''
|
||||
@@ -56,26 +62,43 @@ const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Netlify',
|
||||
slug: 'netlify',
|
||||
image: 'Netlify',
|
||||
image: 'Netlify.png',
|
||||
isAvailable: true,
|
||||
type: 'oauth2',
|
||||
type: 'oauth',
|
||||
clientId: CLIENT_ID_NETLIFY,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'GitHub',
|
||||
slug: 'github',
|
||||
image: 'GitHub',
|
||||
image: 'GitHub.png',
|
||||
isAvailable: true,
|
||||
type: 'oauth2',
|
||||
type: 'oauth',
|
||||
clientId: CLIENT_ID_GITHUB,
|
||||
docsLink: ''
|
||||
|
||||
},
|
||||
{
|
||||
name: 'Render',
|
||||
slug: 'render',
|
||||
image: 'Render.png',
|
||||
isAvailable: true,
|
||||
type: 'pat',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Fly.io',
|
||||
slug: 'flyio',
|
||||
image: 'Flyio.svg',
|
||||
isAvailable: true,
|
||||
type: 'pat',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Google Cloud Platform',
|
||||
slug: 'gcp',
|
||||
image: 'Google Cloud Platform',
|
||||
image: 'Google Cloud Platform.png',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
@@ -84,7 +107,7 @@ const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Amazon Web Services',
|
||||
slug: 'aws',
|
||||
image: 'Amazon Web Services',
|
||||
image: 'Amazon Web Services.png',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
@@ -93,7 +116,7 @@ const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Microsoft Azure',
|
||||
slug: 'azure',
|
||||
image: 'Microsoft Azure',
|
||||
image: 'Microsoft Azure.png',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
@@ -102,7 +125,7 @@ const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Travis CI',
|
||||
slug: 'travisci',
|
||||
image: 'Travis CI',
|
||||
image: 'Travis CI.png',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
@@ -111,7 +134,7 @@ const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Circle CI',
|
||||
slug: 'circleci',
|
||||
image: 'Circle CI',
|
||||
image: 'Circle CI.png',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
@@ -124,6 +147,8 @@ export {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
@@ -133,5 +158,7 @@ export {
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
INTEGRATION_FLYIO_API_URL,
|
||||
INTEGRATION_OPTIONS
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user