Add Render, Fly.io integrations and reduce integrations page reloads
@@ -1,8 +1,11 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import axios from 'axios';
|
import {
|
||||||
import { readFileSync } from 'fs';
|
Integration,
|
||||||
import { IntegrationAuth, Integration } from '../../models';
|
IntegrationAuth,
|
||||||
|
Bot
|
||||||
|
} from '../../models';
|
||||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
|
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
|
||||||
import { IntegrationService } from '../../services';
|
import { IntegrationService } from '../../services';
|
||||||
import { getApps, revokeAccess } from '../../integrations';
|
import { getApps, revokeAccess } from '../../integrations';
|
||||||
@@ -44,7 +47,7 @@ export const oAuthExchange = async (
|
|||||||
environment: environments[0].slug,
|
environment: environments[0].slug,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get OAuth2 code-token exchange'
|
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]
|
* Return list of applications allowed for integration with integration authorization id [integrationAuthId]
|
||||||
* @param req
|
* @param req
|
||||||
@@ -70,7 +134,7 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
|||||||
accessToken: req.accessToken
|
accessToken: req.accessToken
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get integration authorization applications'
|
message: 'Failed to get integration authorization applications'
|
||||||
@@ -89,15 +153,14 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
||||||
|
let integrationAuth;
|
||||||
try {
|
try {
|
||||||
const { integrationAuthId } = req.params;
|
integrationAuth = await revokeAccess({
|
||||||
|
|
||||||
await revokeAccess({
|
|
||||||
integrationAuth: req.integrationAuth,
|
integrationAuth: req.integrationAuth,
|
||||||
accessToken: req.accessToken
|
accessToken: req.accessToken
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to delete integration authorization'
|
message: 'Failed to delete integration authorization'
|
||||||
@@ -105,6 +168,6 @@ export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully deleted integration authorization'
|
integrationAuth
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,43 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Integration, Bot, BotKey } from '../../models';
|
import {
|
||||||
|
Integration,
|
||||||
|
Workspace,
|
||||||
|
Bot,
|
||||||
|
BotKey
|
||||||
|
} from '../../models';
|
||||||
import { EventService } from '../../services';
|
import { EventService } from '../../services';
|
||||||
import { eventPushSecrets } from '../../events';
|
import { eventPushSecrets } from '../../events';
|
||||||
|
|
||||||
interface Key {
|
/**
|
||||||
encryptedKey: string;
|
* Create/initialize an (empty) integration for integration authorization
|
||||||
nonce: string;
|
* @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 {
|
return res.status(200).send({
|
||||||
ciphertextKey: string;
|
integration
|
||||||
ivKey: string;
|
});
|
||||||
tagKey: string;
|
|
||||||
hashKey: string;
|
|
||||||
ciphertextValue: string;
|
|
||||||
ivValue: string;
|
|
||||||
tagValue: string;
|
|
||||||
hashValue: string;
|
|
||||||
type: 'shared' | 'personal';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,13 +54,12 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
app,
|
|
||||||
environment,
|
environment,
|
||||||
isActive,
|
isActive,
|
||||||
target, // vercel-specific integration param
|
app,
|
||||||
context, // netlify-specific integration param
|
appId,
|
||||||
siteId, // netlify-specific integration param
|
targetEnvironment,
|
||||||
owner // github-specific integration param
|
owner, // github-specific integration param
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
integration = await Integration.findOneAndUpdate(
|
integration = await Integration.findOneAndUpdate(
|
||||||
@@ -53,9 +70,8 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
|||||||
environment,
|
environment,
|
||||||
isActive,
|
isActive,
|
||||||
app,
|
app,
|
||||||
target,
|
appId,
|
||||||
context,
|
targetEnvironment,
|
||||||
siteId,
|
|
||||||
owner
|
owner
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -101,27 +117,6 @@ export const deleteIntegration = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!integration) throw new Error('Failed to find integration');
|
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) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ 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,
|
||||||
@@ -142,7 +141,7 @@ const syncIntegrationsHelper = async ({
|
|||||||
workspaceId: integration.workspace.toString(),
|
workspaceId: integration.workspace.toString(),
|
||||||
environment: integration.environment
|
environment: integration.environment
|
||||||
});
|
});
|
||||||
|
|
||||||
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
||||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
@@ -316,7 +315,7 @@ const setIntegrationAuthAccessHelper = async ({
|
|||||||
}: {
|
}: {
|
||||||
integrationAuthId: string;
|
integrationAuthId: string;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
accessExpiresAt: Date;
|
accessExpiresAt: Date | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
let integrationAuth;
|
let integrationAuth;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,9 +7,13 @@ import {
|
|||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
|
INTEGRATION_RENDER,
|
||||||
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_HEROKU_API_URL,
|
INTEGRATION_HEROKU_API_URL,
|
||||||
INTEGRATION_VERCEL_API_URL,
|
INTEGRATION_VERCEL_API_URL,
|
||||||
INTEGRATION_NETLIFY_API_URL
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
|
INTEGRATION_RENDER_API_URL,
|
||||||
|
INTEGRATION_FLYIO_API_URL
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,10 +33,11 @@ const getApps = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
interface App {
|
interface App {
|
||||||
name: string;
|
name: string;
|
||||||
siteId?: string;
|
appId?: string;
|
||||||
|
owner?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let apps: App[]; // TODO: add type and define payloads for apps
|
let apps: App[];
|
||||||
try {
|
try {
|
||||||
switch (integrationAuth.integration) {
|
switch (integrationAuth.integration) {
|
||||||
case INTEGRATION_HEROKU:
|
case INTEGRATION_HEROKU:
|
||||||
@@ -48,13 +53,21 @@ const getApps = async ({
|
|||||||
break;
|
break;
|
||||||
case INTEGRATION_NETLIFY:
|
case INTEGRATION_NETLIFY:
|
||||||
apps = await getAppsNetlify({
|
apps = await getAppsNetlify({
|
||||||
integrationAuth,
|
|
||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case INTEGRATION_GITHUB:
|
case INTEGRATION_GITHUB:
|
||||||
apps = await getAppsGithub({
|
apps = await getAppsGithub({
|
||||||
integrationAuth,
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_RENDER:
|
||||||
|
apps = await getAppsRender({
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_FLYIO:
|
||||||
|
apps = await getAppsFlyio({
|
||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
break;
|
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 {Object} obj
|
||||||
* @param {String} obj.accessToken - access token for Heroku API
|
* @param {String} obj.accessToken - access token for Heroku API
|
||||||
* @returns {Object[]} apps - names of Heroku apps
|
* @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 {Object} obj
|
||||||
* @param {String} obj.accessToken - access token for Netlify API
|
* @param {String} obj.accessToken - access token for Netlify API
|
||||||
* @returns {Object[]} apps - names of Netlify sites
|
* @returns {Object[]} apps - names of Netlify sites
|
||||||
* @returns {String} apps.name - name of Netlify site
|
* @returns {String} apps.name - name of Netlify site
|
||||||
*/
|
*/
|
||||||
const getAppsNetlify = async ({
|
const getAppsNetlify = async ({
|
||||||
integrationAuth,
|
|
||||||
accessToken
|
accessToken
|
||||||
}: {
|
}: {
|
||||||
integrationAuth: IIntegrationAuth;
|
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
let apps;
|
let apps;
|
||||||
@@ -166,7 +177,7 @@ const getAppsNetlify = async ({
|
|||||||
|
|
||||||
apps = res.map((a: any) => ({
|
apps = res.map((a: any) => ({
|
||||||
name: a.name,
|
name: a.name,
|
||||||
siteId: a.site_id
|
appId: a.site_id
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
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 {Object} obj
|
||||||
* @param {String} obj.accessToken - access token for Netlify API
|
* @param {String} obj.accessToken - access token for Netlify API
|
||||||
* @returns {Object[]} apps - names of Netlify sites
|
* @returns {Object[]} apps - names of Netlify sites
|
||||||
* @returns {String} apps.name - name of Netlify site
|
* @returns {String} apps.name - name of Netlify site
|
||||||
*/
|
*/
|
||||||
const getAppsGithub = async ({
|
const getAppsGithub = async ({
|
||||||
integrationAuth,
|
|
||||||
accessToken
|
accessToken
|
||||||
}: {
|
}: {
|
||||||
integrationAuth: IIntegrationAuth;
|
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
let apps;
|
let apps;
|
||||||
@@ -220,4 +229,94 @@ const getAppsGithub = async ({
|
|||||||
return apps;
|
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 };
|
export { getApps };
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { IIntegrationAuth, IntegrationAuth, Integration } from '../models';
|
import {
|
||||||
|
IIntegrationAuth,
|
||||||
|
IntegrationAuth,
|
||||||
|
Integration,
|
||||||
|
Bot,
|
||||||
|
BotKey
|
||||||
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
@@ -15,6 +20,7 @@ const revokeAccess = async ({
|
|||||||
integrationAuth: IIntegrationAuth;
|
integrationAuth: IIntegrationAuth;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
let deletedIntegrationAuth;
|
||||||
try {
|
try {
|
||||||
// add any integration-specific revocation logic
|
// add any integration-specific revocation logic
|
||||||
switch (integrationAuth.integration) {
|
switch (integrationAuth.integration) {
|
||||||
@@ -28,7 +34,7 @@ const revokeAccess = async ({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||||
_id: integrationAuth._id
|
_id: integrationAuth._id
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,6 +48,8 @@ const revokeAccess = async ({
|
|||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to delete integration authorization');
|
throw new Error('Failed to delete integration authorization');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return deletedIntegrationAuth;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { revokeAccess };
|
export { revokeAccess };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import axios, { AxiosError } from 'axios';
|
import axios from 'axios';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Octokit } from '@octokit/rest';
|
import { Octokit } from '@octokit/rest';
|
||||||
// import * as sodium from 'libsodium-wrappers';
|
// import * as sodium from 'libsodium-wrappers';
|
||||||
@@ -10,9 +10,13 @@ import {
|
|||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
|
INTEGRATION_RENDER,
|
||||||
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_HEROKU_API_URL,
|
INTEGRATION_HEROKU_API_URL,
|
||||||
INTEGRATION_VERCEL_API_URL,
|
INTEGRATION_VERCEL_API_URL,
|
||||||
INTEGRATION_NETLIFY_API_URL
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
|
INTEGRATION_RENDER_API_URL,
|
||||||
|
INTEGRATION_FLYIO_API_URL
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import { access, appendFile } from 'fs';
|
import { access, appendFile } from 'fs';
|
||||||
|
|
||||||
@@ -21,8 +25,6 @@ import { access, appendFile } from 'fs';
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {IIntegration} obj.integration - integration details
|
* @param {IIntegration} obj.integration - integration details
|
||||||
* @param {IIntegrationAuth} obj.integrationAuth - integration auth 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 {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
|
* @param {String} obj.accessToken - access token for integration
|
||||||
*/
|
*/
|
||||||
@@ -69,6 +71,20 @@ const syncSecrets = async ({
|
|||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case INTEGRATION_RENDER:
|
||||||
|
await syncSecretsRender({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_FLYIO:
|
||||||
|
await syncSecretsFlyio({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
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 {Object} obj
|
||||||
* @param {IIntegration} obj.integration - integration details
|
* @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)
|
||||||
|
* @param {String} obj.accessToken - access token for Heroku integration
|
||||||
*/
|
*/
|
||||||
const syncSecretsHeroku = async ({
|
const syncSecretsHeroku = async ({
|
||||||
integration,
|
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 {Object} obj
|
||||||
* @param {IIntegration} obj.integration - integration details
|
* @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)
|
||||||
@@ -174,7 +191,7 @@ const syncSecretsVercel = async ({
|
|||||||
))
|
))
|
||||||
.data
|
.data
|
||||||
.envs
|
.envs
|
||||||
.filter((secret: VercelSecret) => secret.target.includes(integration.target))
|
.filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment))
|
||||||
.map(async (secret: VercelSecret) => (await axios.get(
|
.map(async (secret: VercelSecret) => (await axios.get(
|
||||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||||
{
|
{
|
||||||
@@ -201,7 +218,7 @@ const syncSecretsVercel = async ({
|
|||||||
key: key,
|
key: key,
|
||||||
value: secrets[key],
|
value: secrets[key],
|
||||||
type: 'encrypted',
|
type: 'encrypted',
|
||||||
target: [integration.target]
|
target: [integration.targetEnvironment]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -216,7 +233,7 @@ const syncSecretsVercel = async ({
|
|||||||
key: key,
|
key: key,
|
||||||
value: secrets[key],
|
value: secrets[key],
|
||||||
type: 'encrypted',
|
type: 'encrypted',
|
||||||
target: [integration.target]
|
target: [integration.targetEnvironment]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -226,7 +243,7 @@ const syncSecretsVercel = async ({
|
|||||||
key: key,
|
key: key,
|
||||||
value: res[key].value,
|
value: res[key].value,
|
||||||
type: 'encrypted',
|
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 {Object} obj
|
||||||
* @param {IIntegration} obj.integration - integration details
|
* @param {IIntegration} obj.integration - integration details
|
||||||
* @param {IIntegrationAuth} obj.integrationAuth - integration auth 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.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 ({
|
const syncSecretsNetlify = async ({
|
||||||
integration,
|
integration,
|
||||||
@@ -323,7 +341,7 @@ const syncSecretsNetlify = async ({
|
|||||||
|
|
||||||
const getParams = new URLSearchParams({
|
const getParams = new URLSearchParams({
|
||||||
context_name: 'all', // integration.context or all
|
context_name: 'all', // integration.context or all
|
||||||
site_id: integration.siteId
|
site_id: integration.appId
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = (await axios.get(
|
const res = (await axios.get(
|
||||||
@@ -354,7 +372,7 @@ const syncSecretsNetlify = async ({
|
|||||||
key,
|
key,
|
||||||
values: [{
|
values: [{
|
||||||
value: secrets[key],
|
value: secrets[key],
|
||||||
context: integration.context
|
context: integration.targetEnvironment
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -365,15 +383,15 @@ const syncSecretsNetlify = async ({
|
|||||||
[value.context]: value
|
[value.context]: value
|
||||||
}), {});
|
}), {});
|
||||||
|
|
||||||
if (integration.context in contexts) {
|
if (integration.targetEnvironment in contexts) {
|
||||||
// case: Netlify secret value exists in integration context
|
// 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
|
// case: Infisical and Netlify secret values are different
|
||||||
// -> update Netlify secret context and value
|
// -> update Netlify secret context and value
|
||||||
updateSecrets.push({
|
updateSecrets.push({
|
||||||
key,
|
key,
|
||||||
values: [{
|
values: [{
|
||||||
context: integration.context,
|
context: integration.targetEnvironment,
|
||||||
value: secrets[key]
|
value: secrets[key]
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -384,7 +402,7 @@ const syncSecretsNetlify = async ({
|
|||||||
updateSecrets.push({
|
updateSecrets.push({
|
||||||
key,
|
key,
|
||||||
values: [{
|
values: [{
|
||||||
context: integration.context,
|
context: integration.targetEnvironment,
|
||||||
value: secrets[key]
|
value: secrets[key]
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -402,7 +420,7 @@ const syncSecretsNetlify = async ({
|
|||||||
const numberOfValues = res[key].values.length;
|
const numberOfValues = res[key].values.length;
|
||||||
|
|
||||||
res[key].values.forEach((value: NetlifyValue) => {
|
res[key].values.forEach((value: NetlifyValue) => {
|
||||||
if (value.context === integration.context) {
|
if (value.context === integration.targetEnvironment) {
|
||||||
if (numberOfValues <= 1) {
|
if (numberOfValues <= 1) {
|
||||||
// case: Netlify secret value has less than 1 context -> delete secret
|
// case: Netlify secret value has less than 1 context -> delete secret
|
||||||
deleteSecrets.push(key);
|
deleteSecrets.push(key);
|
||||||
@@ -412,7 +430,7 @@ const syncSecretsNetlify = async ({
|
|||||||
key,
|
key,
|
||||||
values: [{
|
values: [{
|
||||||
id: value.id,
|
id: value.id,
|
||||||
context: integration.context,
|
context: integration.targetEnvironment,
|
||||||
value: value.value
|
value: value.value
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -423,7 +441,7 @@ const syncSecretsNetlify = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const syncParams = new URLSearchParams({
|
const syncParams = new URLSearchParams({
|
||||||
site_id: integration.siteId
|
site_id: integration.appId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newSecrets.length > 0) {
|
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 {Object} obj
|
||||||
* @param {IIntegration} obj.integration - integration details
|
* @param {IIntegration} obj.integration - integration details
|
||||||
* @param {IIntegrationAuth} obj.integrationAuth - integration auth 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.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 ({
|
const syncSecretsGitHub = async ({
|
||||||
integration,
|
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 };
|
export { syncSecrets };
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { IntegrationAuth } from '../models';
|
import { IntegrationAuth, IWorkspace } from '../models';
|
||||||
import { IntegrationService } from '../services';
|
import { IntegrationService } from '../services';
|
||||||
import { validateMembership } from '../helpers/membership';
|
import { validateMembership } from '../helpers/membership';
|
||||||
import { UnauthorizedRequestError } from '../utils/errors';
|
import { UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
|
||||||
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate if user on request is a member of workspace with proper roles associated
|
* Validate if user on request is a member of workspace with proper roles associated
|
||||||
* with the integration authorization on request params.
|
* with the integration authorization on request params.
|
||||||
@@ -14,17 +16,21 @@ import { UnauthorizedRequestError } from '../utils/errors';
|
|||||||
*/
|
*/
|
||||||
const requireIntegrationAuthorizationAuth = ({
|
const requireIntegrationAuthorizationAuth = ({
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
attachAccessToken = true
|
attachAccessToken = true,
|
||||||
|
location = 'params'
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: string[];
|
||||||
attachAccessToken?: boolean;
|
attachAccessToken?: boolean;
|
||||||
|
location?: req;
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const { integrationAuthId } = req.params;
|
const { integrationAuthId } = req[location];
|
||||||
|
|
||||||
const integrationAuth = await IntegrationAuth.findOne({
|
const integrationAuth = await IntegrationAuth.findOne({
|
||||||
_id: integrationAuthId
|
_id: integrationAuthId
|
||||||
}).select(
|
})
|
||||||
|
.populate<{ workspace: IWorkspace }>('workspace')
|
||||||
|
.select(
|
||||||
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
|
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -34,7 +40,7 @@ const requireIntegrationAuthorizationAuth = ({
|
|||||||
|
|
||||||
await validateMembership({
|
await validateMembership({
|
||||||
userId: req.user._id.toString(),
|
userId: req.user._id.toString(),
|
||||||
workspaceId: integrationAuth.workspace.toString(),
|
workspaceId: integrationAuth.workspace._id.toString(),
|
||||||
acceptedRoles
|
acceptedRoles
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import {
|
|||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB
|
INTEGRATION_GITHUB,
|
||||||
|
INTEGRATION_RENDER,
|
||||||
|
INTEGRATION_FLYIO
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
export interface IIntegration {
|
export interface IIntegration {
|
||||||
@@ -12,20 +14,19 @@ export interface IIntegration {
|
|||||||
environment: string;
|
environment: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
app: string;
|
app: string;
|
||||||
target: string;
|
|
||||||
context: string;
|
|
||||||
siteId: string;
|
|
||||||
owner: string;
|
owner: string;
|
||||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github';
|
targetEnvironment: string;
|
||||||
|
appId: string;
|
||||||
|
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
|
||||||
integrationAuth: Types.ObjectId;
|
integrationAuth: Types.ObjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const integrationSchema = new Schema<IIntegration>(
|
const integrationSchema = new Schema<IIntegration>(
|
||||||
{
|
{
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Workspace',
|
ref: 'Workspace',
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -40,18 +41,13 @@ const integrationSchema = new Schema<IIntegration>(
|
|||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
target: {
|
appId: { // (new)
|
||||||
// vercel-specific target (environment)
|
// id of app in provider
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
context: {
|
targetEnvironment: { // (new)
|
||||||
// netlify-specific context (deploy)
|
// target environment
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
siteId: {
|
|
||||||
// netlify-specific site (app) id
|
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
@@ -66,7 +62,9 @@ const integrationSchema = new Schema<IIntegration>(
|
|||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB
|
INTEGRATION_GITHUB,
|
||||||
|
INTEGRATION_RENDER,
|
||||||
|
INTEGRATION_FLYIO
|
||||||
],
|
],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
export interface IIntegrationAuth {
|
export interface IIntegrationAuth {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github';
|
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
|
||||||
teamId: string;
|
teamId: string;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
refreshCiphertext?: string;
|
refreshCiphertext?: string;
|
||||||
@@ -24,8 +24,9 @@ export interface IIntegrationAuth {
|
|||||||
const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
||||||
{
|
{
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
required: true
|
ref: 'Workspace',
|
||||||
|
required: true
|
||||||
},
|
},
|
||||||
integration: {
|
integration: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -3,12 +3,27 @@ const router = express.Router();
|
|||||||
import {
|
import {
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireIntegrationAuth,
|
requireIntegrationAuth,
|
||||||
|
requireIntegrationAuthorizationAuth,
|
||||||
validateRequest
|
validateRequest
|
||||||
} from '../../middleware';
|
} from '../../middleware';
|
||||||
import { ADMIN, MEMBER } from '../../variables';
|
import { ADMIN, MEMBER } from '../../variables';
|
||||||
import { body, param } from 'express-validator';
|
import { body, param } from 'express-validator';
|
||||||
import { integrationController } from '../../controllers/v1';
|
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(
|
router.patch(
|
||||||
'/:integrationId',
|
'/:integrationId',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
@@ -18,12 +33,11 @@ router.patch(
|
|||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
}),
|
}),
|
||||||
param('integrationId').exists().trim(),
|
param('integrationId').exists().trim(),
|
||||||
|
body('isActive').exists().isBoolean(),
|
||||||
body('app').exists().trim(),
|
body('app').exists().trim(),
|
||||||
body('environment').exists().trim(),
|
body('environment').exists().trim(),
|
||||||
body('isActive').exists().isBoolean(),
|
body('appId').exists(),
|
||||||
body('target').exists(),
|
body('targetEnvironment').exists(),
|
||||||
body('context').exists(),
|
|
||||||
body('siteId').exists(),
|
|
||||||
body('owner').exists(),
|
body('owner').exists(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
integrationController.updateIntegration
|
integrationController.updateIntegration
|
||||||
|
|||||||
@@ -34,6 +34,22 @@ router.post(
|
|||||||
integrationAuthController.oAuthExchange
|
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(
|
router.get(
|
||||||
'/:integrationAuthId/apps',
|
'/:integrationAuthId/apps',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class IntegrationService {
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.integrationAuthId - id of integration auth
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
* @param {String} obj.accessToken - access token
|
* @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
|
* @returns {IntegrationAuth} - updated integration auth
|
||||||
*/
|
*/
|
||||||
static async setIntegrationAuthAccess({
|
static async setIntegrationAuthAccess({
|
||||||
@@ -132,7 +132,7 @@ class IntegrationService {
|
|||||||
}: {
|
}: {
|
||||||
integrationAuthId: string;
|
integrationAuthId: string;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
accessExpiresAt: Date;
|
accessExpiresAt: Date | undefined;
|
||||||
}) {
|
}) {
|
||||||
return await setIntegrationAuthAccessHelper({
|
return await setIntegrationAuthAccessHelper({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
|
INTEGRATION_RENDER,
|
||||||
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
@@ -19,6 +21,8 @@ import {
|
|||||||
INTEGRATION_HEROKU_API_URL,
|
INTEGRATION_HEROKU_API_URL,
|
||||||
INTEGRATION_VERCEL_API_URL,
|
INTEGRATION_VERCEL_API_URL,
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
|
INTEGRATION_RENDER_API_URL,
|
||||||
|
INTEGRATION_FLYIO_API_URL,
|
||||||
INTEGRATION_OPTIONS
|
INTEGRATION_OPTIONS
|
||||||
} from './integration';
|
} from './integration';
|
||||||
import {
|
import {
|
||||||
@@ -56,6 +60,8 @@ export {
|
|||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
|
INTEGRATION_RENDER,
|
||||||
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
@@ -65,6 +71,8 @@ export {
|
|||||||
INTEGRATION_HEROKU_API_URL,
|
INTEGRATION_HEROKU_API_URL,
|
||||||
INTEGRATION_VERCEL_API_URL,
|
INTEGRATION_VERCEL_API_URL,
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
|
INTEGRATION_RENDER_API_URL,
|
||||||
|
INTEGRATION_FLYIO_API_URL,
|
||||||
EVENT_PUSH_SECRETS,
|
EVENT_PUSH_SECRETS,
|
||||||
EVENT_PULL_SECRETS,
|
EVENT_PULL_SECRETS,
|
||||||
ACTION_ADD_SECRETS,
|
ACTION_ADD_SECRETS,
|
||||||
|
|||||||
@@ -10,11 +10,15 @@ const INTEGRATION_HEROKU = 'heroku';
|
|||||||
const INTEGRATION_VERCEL = 'vercel';
|
const INTEGRATION_VERCEL = 'vercel';
|
||||||
const INTEGRATION_NETLIFY = 'netlify';
|
const INTEGRATION_NETLIFY = 'netlify';
|
||||||
const INTEGRATION_GITHUB = 'github';
|
const INTEGRATION_GITHUB = 'github';
|
||||||
|
const INTEGRATION_RENDER = 'render';
|
||||||
|
const INTEGRATION_FLYIO = 'flyio';
|
||||||
const INTEGRATION_SET = new Set([
|
const INTEGRATION_SET = new Set([
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB
|
INTEGRATION_GITHUB,
|
||||||
|
INTEGRATION_RENDER,
|
||||||
|
INTEGRATION_FLYIO
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// integration types
|
// integration types
|
||||||
@@ -32,23 +36,25 @@ const INTEGRATION_GITHUB_TOKEN_URL =
|
|||||||
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
|
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
|
||||||
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
|
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
|
||||||
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.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 = [
|
const INTEGRATION_OPTIONS = [
|
||||||
{
|
{
|
||||||
name: 'Heroku',
|
name: 'Heroku',
|
||||||
slug: 'heroku',
|
slug: 'heroku',
|
||||||
image: 'Heroku',
|
image: 'Heroku.png',
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
type: 'oauth2',
|
type: 'oauth',
|
||||||
clientId: CLIENT_ID_HEROKU,
|
clientId: CLIENT_ID_HEROKU,
|
||||||
docsLink: ''
|
docsLink: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Vercel',
|
name: 'Vercel',
|
||||||
slug: 'vercel',
|
slug: 'vercel',
|
||||||
image: 'Vercel',
|
image: 'Vercel.png',
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
type: 'vercel',
|
type: 'oauth',
|
||||||
clientId: '',
|
clientId: '',
|
||||||
clientSlug: CLIENT_SLUG_VERCEL,
|
clientSlug: CLIENT_SLUG_VERCEL,
|
||||||
docsLink: ''
|
docsLink: ''
|
||||||
@@ -56,26 +62,43 @@ const INTEGRATION_OPTIONS = [
|
|||||||
{
|
{
|
||||||
name: 'Netlify',
|
name: 'Netlify',
|
||||||
slug: 'netlify',
|
slug: 'netlify',
|
||||||
image: 'Netlify',
|
image: 'Netlify.png',
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
type: 'oauth2',
|
type: 'oauth',
|
||||||
clientId: CLIENT_ID_NETLIFY,
|
clientId: CLIENT_ID_NETLIFY,
|
||||||
docsLink: ''
|
docsLink: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'GitHub',
|
name: 'GitHub',
|
||||||
slug: 'github',
|
slug: 'github',
|
||||||
image: 'GitHub',
|
image: 'GitHub.png',
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
type: 'oauth2',
|
type: 'oauth',
|
||||||
clientId: CLIENT_ID_GITHUB,
|
clientId: CLIENT_ID_GITHUB,
|
||||||
docsLink: ''
|
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',
|
name: 'Google Cloud Platform',
|
||||||
slug: 'gcp',
|
slug: 'gcp',
|
||||||
image: 'Google Cloud Platform',
|
image: 'Google Cloud Platform.png',
|
||||||
isAvailable: false,
|
isAvailable: false,
|
||||||
type: '',
|
type: '',
|
||||||
clientId: '',
|
clientId: '',
|
||||||
@@ -84,7 +107,7 @@ const INTEGRATION_OPTIONS = [
|
|||||||
{
|
{
|
||||||
name: 'Amazon Web Services',
|
name: 'Amazon Web Services',
|
||||||
slug: 'aws',
|
slug: 'aws',
|
||||||
image: 'Amazon Web Services',
|
image: 'Amazon Web Services.png',
|
||||||
isAvailable: false,
|
isAvailable: false,
|
||||||
type: '',
|
type: '',
|
||||||
clientId: '',
|
clientId: '',
|
||||||
@@ -93,7 +116,7 @@ const INTEGRATION_OPTIONS = [
|
|||||||
{
|
{
|
||||||
name: 'Microsoft Azure',
|
name: 'Microsoft Azure',
|
||||||
slug: 'azure',
|
slug: 'azure',
|
||||||
image: 'Microsoft Azure',
|
image: 'Microsoft Azure.png',
|
||||||
isAvailable: false,
|
isAvailable: false,
|
||||||
type: '',
|
type: '',
|
||||||
clientId: '',
|
clientId: '',
|
||||||
@@ -102,7 +125,7 @@ const INTEGRATION_OPTIONS = [
|
|||||||
{
|
{
|
||||||
name: 'Travis CI',
|
name: 'Travis CI',
|
||||||
slug: 'travisci',
|
slug: 'travisci',
|
||||||
image: 'Travis CI',
|
image: 'Travis CI.png',
|
||||||
isAvailable: false,
|
isAvailable: false,
|
||||||
type: '',
|
type: '',
|
||||||
clientId: '',
|
clientId: '',
|
||||||
@@ -111,7 +134,7 @@ const INTEGRATION_OPTIONS = [
|
|||||||
{
|
{
|
||||||
name: 'Circle CI',
|
name: 'Circle CI',
|
||||||
slug: 'circleci',
|
slug: 'circleci',
|
||||||
image: 'Circle CI',
|
image: 'Circle CI.png',
|
||||||
isAvailable: false,
|
isAvailable: false,
|
||||||
type: '',
|
type: '',
|
||||||
clientId: '',
|
clientId: '',
|
||||||
@@ -124,6 +147,8 @@ export {
|
|||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
|
INTEGRATION_RENDER,
|
||||||
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
INTEGRATION_HEROKU_TOKEN_URL,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
@@ -133,5 +158,7 @@ export {
|
|||||||
INTEGRATION_HEROKU_API_URL,
|
INTEGRATION_HEROKU_API_URL,
|
||||||
INTEGRATION_VERCEL_API_URL,
|
INTEGRATION_VERCEL_API_URL,
|
||||||
INTEGRATION_NETLIFY_API_URL,
|
INTEGRATION_NETLIFY_API_URL,
|
||||||
|
INTEGRATION_RENDER_API_URL,
|
||||||
|
INTEGRATION_FLYIO_API_URL,
|
||||||
INTEGRATION_OPTIONS
|
INTEGRATION_OPTIONS
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ We're still early with integrations, but expect more soon.
|
|||||||
View all available integrations and their guides
|
View all available integrations and their guides
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
|
After Width: | Height: | Size: 350 KiB |
|
After Width: | Height: | Size: 285 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 398 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 402 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 404 KiB |
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 403 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 418 KiB |
@@ -2,4 +2,34 @@
|
|||||||
title: "Fly.io"
|
title: "Fly.io"
|
||||||
---
|
---
|
||||||
|
|
||||||
Coming soon.
|
Prerequisites:
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
|
||||||
|
## Navigate to your project's integrations tab
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Authorize Infisical for Fly.io
|
||||||
|
|
||||||
|
Obtain a Fly.io access token in Access Tokens
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Press on the Fly.io tile and input your Fly.io access token to grant Infisical access to your Fly.io account.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Info>
|
||||||
|
If this is your project's first cloud integration, then you'll have to grant
|
||||||
|
Infisical access to your project's environment variables. Although this step
|
||||||
|
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||||
|
the cloud platform.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
## Start integration
|
||||||
|
|
||||||
|
Select which Infisical environment secrets you want to sync to which Fly.io app and press start integration to start syncing secrets to Fly.io.
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -2,4 +2,34 @@
|
|||||||
title: "Render"
|
title: "Render"
|
||||||
---
|
---
|
||||||
|
|
||||||
Coming soon.
|
Prerequisites:
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
|
||||||
|
## Navigate to your project's integrations tab
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Enter your Render API Key
|
||||||
|
|
||||||
|
Obtain a Render API Key in your Render Account Settings > API Keys.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Press on the Render tile and input your Render API Key to grant Infisical access to your Render account.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Info>
|
||||||
|
If this is your project's first cloud integration, then you'll have to grant
|
||||||
|
Infisical access to your project's environment variables. Although this step
|
||||||
|
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||||
|
the cloud platform.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
## Start integration
|
||||||
|
|
||||||
|
Select which Infisical environment secrets you want to sync to which Render service and press start integration to start syncing secrets to Render.
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -20,4 +20,4 @@ Press on the Vercel tile and grant Infisical access to your Vercel account.
|
|||||||
|
|
||||||
Select which Infisical environment secrets you want to sync to which Vercel app and environment. Lastly, press start integration to start syncing secrets to Vercel.
|
Select which Infisical environment secrets you want to sync to which Vercel app and environment. Lastly, press start integration to start syncing secrets to Vercel.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
@@ -4,12 +4,23 @@ import { Dialog, Transition } from '@headlessui/react';
|
|||||||
|
|
||||||
import Button from '../buttons/Button';
|
import Button from '../buttons/Button';
|
||||||
|
|
||||||
|
interface IntegrationOption {
|
||||||
|
clientId: string;
|
||||||
|
clientSlug?: string; // vercel-integration specific
|
||||||
|
docsLink: string;
|
||||||
|
image: string;
|
||||||
|
isAvailable: boolean;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
selectedIntegrationOption: never[] | null;
|
selectedIntegrationOption: IntegrationOption | null;
|
||||||
handleBotActivate: () => Promise<void>;
|
handleBotActivate: () => Promise<void>;
|
||||||
handleIntegrationOption: (arg: { integrationOption: never[] }) => void;
|
integrationOptionPress: (integrationOption: IntegrationOption) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ActivateBotDialog = ({
|
const ActivateBotDialog = ({
|
||||||
@@ -17,7 +28,7 @@ const ActivateBotDialog = ({
|
|||||||
closeModal,
|
closeModal,
|
||||||
selectedIntegrationOption,
|
selectedIntegrationOption,
|
||||||
handleBotActivate,
|
handleBotActivate,
|
||||||
handleIntegrationOption
|
integrationOptionPress
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -28,10 +39,10 @@ const ActivateBotDialog = ({
|
|||||||
|
|
||||||
// type check
|
// type check
|
||||||
if (!selectedIntegrationOption) return;
|
if (!selectedIntegrationOption) return;
|
||||||
// 2. start integration
|
|
||||||
await handleIntegrationOption({
|
// 2. start integration or probe for PAT
|
||||||
integrationOption: selectedIntegrationOption
|
integrationOptionPress(selectedIntegrationOption);
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,60 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
|
import Button from "../buttons/Button";
|
||||||
import InputField from "../InputField";
|
import InputField from "../InputField";
|
||||||
|
|
||||||
|
interface IntegrationOption {
|
||||||
|
clientId: string;
|
||||||
|
clientSlug?: string; // vercel-integration specific
|
||||||
|
docsLink: string;
|
||||||
|
image: string;
|
||||||
|
isAvailable: boolean;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
selectedIntegrationOption: string;
|
selectedIntegrationOption: IntegrationOption | null
|
||||||
handleBotActivate: () => void;
|
handleIntegrationOption: (arg:{
|
||||||
handleIntegrationOption: (arg:{integrationOption:string})=>void;
|
integrationOption: IntegrationOption,
|
||||||
|
accessToken?: string;
|
||||||
|
})=>void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IntegrationAccessTokenDialog = ({
|
const IntegrationAccessTokenDialog = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
closeModal,
|
closeModal,
|
||||||
selectedIntegrationOption,
|
selectedIntegrationOption,
|
||||||
handleBotActivate,
|
|
||||||
handleIntegrationOption
|
handleIntegrationOption
|
||||||
}:Props) => {
|
}:Props) => {
|
||||||
|
const [accessToken, setAccessToken] = useState('');
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
try {
|
try {
|
||||||
// 1. activate bot
|
if (selectedIntegrationOption && accessToken !== '') {
|
||||||
await handleBotActivate();
|
handleIntegrationOption({
|
||||||
|
integrationOption: selectedIntegrationOption,
|
||||||
// 2. start integration
|
accessToken
|
||||||
await handleIntegrationOption({
|
});
|
||||||
integrationOption: selectedIntegrationOption
|
closeModal();
|
||||||
});
|
setAccessToken('');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
closeModal();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
<Dialog as="div" className="relative z-10" onClose={() => {
|
||||||
|
console.log('onClose');
|
||||||
|
closeModal();
|
||||||
|
}}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
@@ -67,28 +82,30 @@ const IntegrationAccessTokenDialog = ({
|
|||||||
as="h3"
|
as="h3"
|
||||||
className="text-lg font-medium leading-6 text-gray-400"
|
className="text-lg font-medium leading-6 text-gray-400"
|
||||||
>
|
>
|
||||||
Grant Infisical access to your secrets
|
{`Enter your ${selectedIntegrationOption?.name} API Key`}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2 mb-2">
|
<div className="mt-2 mb-2">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Most cloud integrations require Infisical to be able to decrypt your secrets so they can be forwarded over.
|
{`This integration requires you to obtain an API key from ${selectedIntegrationOption?.name ?? ''} and store it with Infisical.`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 max-w-max">
|
<div className="mt-6 max-w-max">
|
||||||
{/* <Button
|
|
||||||
onButtonPressed={submit}
|
|
||||||
color="mineshaft"
|
|
||||||
text="Grant access"
|
|
||||||
size="md"
|
|
||||||
/> */}
|
|
||||||
<InputField
|
<InputField
|
||||||
label="Access token"
|
label="API Key"
|
||||||
onChangeHandler={() => {}}
|
onChangeHandler={setAccessToken}
|
||||||
type="varName"
|
type="varName"
|
||||||
value="Hello"
|
value={accessToken}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
isRequired
|
isRequired
|
||||||
/>
|
/>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button
|
||||||
|
onButtonPressed={submit}
|
||||||
|
color="mineshaft"
|
||||||
|
text="Connect"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|||||||
@@ -1,38 +1,42 @@
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
import deleteIntegrationAuth from '../../pages/api/integrations/DeleteIntegrationAuth';
|
import deleteIntegrationAuth from '../../pages/api/integrations/DeleteIntegrationAuth';
|
||||||
|
|
||||||
interface CloudIntegrationOption {
|
interface IntegrationOption {
|
||||||
|
clientId: string;
|
||||||
|
clientSlug?: string; // vercel-integration specific
|
||||||
|
docsLink: string;
|
||||||
|
image: string;
|
||||||
isAvailable: boolean;
|
isAvailable: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
|
||||||
clientId: string;
|
|
||||||
docsLink: string;
|
|
||||||
slug: string;
|
slug: string;
|
||||||
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IntegrationAuth {
|
interface IntegrationAuth {
|
||||||
_id: string;
|
_id: string;
|
||||||
integration: string;
|
integration: string;
|
||||||
|
workspace: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cloudIntegrationOption: CloudIntegrationOption;
|
cloudIntegrationOption: IntegrationOption;
|
||||||
setSelectedIntegrationOption: (cloudIntegration: CloudIntegrationOption) => void;
|
setSelectedIntegrationOption: (cloudIntegration: IntegrationOption) => void;
|
||||||
integrationOptionPress: (cloudIntegrationOption: CloudIntegrationOption) => void;
|
integrationOptionPress: (cloudIntegrationOption: IntegrationOption) => void;
|
||||||
integrationAuths: IntegrationAuth[];
|
integrationAuths: IntegrationAuth[];
|
||||||
|
handleDeleteIntegrationAuth: (args: { integrationAuth: IntegrationAuth }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CloudIntegration = ({
|
const CloudIntegration = ({
|
||||||
cloudIntegrationOption,
|
cloudIntegrationOption,
|
||||||
setSelectedIntegrationOption,
|
setSelectedIntegrationOption,
|
||||||
integrationOptionPress,
|
integrationOptionPress,
|
||||||
integrationAuths
|
integrationAuths,
|
||||||
|
handleDeleteIntegrationAuth
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const router = useRouter();
|
|
||||||
return integrationAuths ? (
|
return integrationAuths ? (
|
||||||
<div
|
<div
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
@@ -51,7 +55,7 @@ const CloudIntegration = ({
|
|||||||
key={cloudIntegrationOption.name}
|
key={cloudIntegrationOption.name}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={`/images/integrations/${cloudIntegrationOption.name}.png`}
|
src={`/images/integrations/${cloudIntegrationOption.image}`}
|
||||||
height={70}
|
height={70}
|
||||||
width={70}
|
width={70}
|
||||||
alt="integration logo"
|
alt="integration logo"
|
||||||
@@ -71,24 +75,26 @@ const CloudIntegration = ({
|
|||||||
{cloudIntegrationOption.isAvailable &&
|
{cloudIntegrationOption.isAvailable &&
|
||||||
integrationAuths
|
integrationAuths
|
||||||
.map((authorization) => authorization.integration)
|
.map((authorization) => authorization.integration)
|
||||||
.includes(cloudIntegrationOption.name.toLowerCase()) && (
|
.includes(cloudIntegrationOption.slug) && (
|
||||||
<div className="absolute group z-40 top-0 right-0 flex flex-row">
|
<div className="absolute group z-40 top-0 right-0 flex flex-row">
|
||||||
<div
|
<div
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={(event) => {
|
onClick={async (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
deleteIntegrationAuth({
|
const deletedIntegrationAuth = await deleteIntegrationAuth({
|
||||||
integrationAuthId: integrationAuths
|
integrationAuthId: integrationAuths
|
||||||
.filter(
|
.filter(
|
||||||
(authorization) =>
|
(authorization) =>
|
||||||
authorization.integration === cloudIntegrationOption.name.toLowerCase()
|
authorization.integration === cloudIntegrationOption.slug
|
||||||
)
|
)
|
||||||
.map((authorization) => authorization._id)[0]
|
.map((authorization) => authorization._id)[0]
|
||||||
});
|
});
|
||||||
|
|
||||||
router.reload();
|
handleDeleteIntegrationAuth({
|
||||||
|
integrationAuth: deletedIntegrationAuth
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200"
|
className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,20 +2,31 @@ import { useTranslation } from 'next-i18next';
|
|||||||
|
|
||||||
import CloudIntegration from './CloudIntegration';
|
import CloudIntegration from './CloudIntegration';
|
||||||
|
|
||||||
interface CloudIntegrationOption {
|
interface IntegrationOption {
|
||||||
|
clientId: string;
|
||||||
|
clientSlug?: string; // vercel-integration specific
|
||||||
|
docsLink: string;
|
||||||
|
image: string;
|
||||||
isAvailable: boolean;
|
isAvailable: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
|
||||||
clientId: string;
|
|
||||||
docsLink: string;
|
|
||||||
slug: string;
|
slug: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntegrationAuth {
|
||||||
|
_id: string;
|
||||||
|
integration: string;
|
||||||
|
workspace: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cloudIntegrationOptions: CloudIntegrationOption[];
|
cloudIntegrationOptions: IntegrationOption[];
|
||||||
setSelectedIntegrationOption: () => void;
|
setSelectedIntegrationOption: () => void;
|
||||||
integrationOptionPress: () => void;
|
integrationOptionPress: (integrationOption: IntegrationOption) => void;
|
||||||
integrationAuths: any;
|
integrationAuths: IntegrationAuth[];
|
||||||
|
handleDeleteIntegrationAuth: (args: { integrationAuth: IntegrationAuth }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CloudIntegrationSection = ({
|
const CloudIntegrationSection = ({
|
||||||
@@ -23,6 +34,7 @@ const CloudIntegrationSection = ({
|
|||||||
setSelectedIntegrationOption,
|
setSelectedIntegrationOption,
|
||||||
integrationOptionPress,
|
integrationOptionPress,
|
||||||
integrationAuths,
|
integrationAuths,
|
||||||
|
handleDeleteIntegrationAuth
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -45,6 +57,7 @@ const CloudIntegrationSection = ({
|
|||||||
setSelectedIntegrationOption={setSelectedIntegrationOption}
|
setSelectedIntegrationOption={setSelectedIntegrationOption}
|
||||||
integrationOptionPress={integrationOptionPress}
|
integrationOptionPress={integrationOptionPress}
|
||||||
integrationAuths={integrationAuths}
|
integrationAuths={integrationAuths}
|
||||||
|
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
|
||||||
key={`cloud-integration-${cloudIntegrationOption.slug}`}
|
key={`cloud-integration-${cloudIntegrationOption.slug}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -13,39 +13,44 @@ import deleteIntegration from '../../pages/api/integrations/DeleteIntegration';
|
|||||||
import getIntegrationApps from '../../pages/api/integrations/GetIntegrationApps';
|
import getIntegrationApps from '../../pages/api/integrations/GetIntegrationApps';
|
||||||
import updateIntegration from '../../pages/api/integrations/updateIntegration';
|
import updateIntegration from '../../pages/api/integrations/updateIntegration';
|
||||||
|
|
||||||
interface TIntegration {
|
interface Integration {
|
||||||
_id: string;
|
_id: string;
|
||||||
app?: string;
|
isActive: boolean;
|
||||||
target?: string;
|
app: string | null;
|
||||||
|
appId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
integration: string;
|
integration: string;
|
||||||
|
targetEnvironment: string;
|
||||||
|
workspace: string;
|
||||||
integrationAuth: string;
|
integrationAuth: string;
|
||||||
isActive: boolean;
|
|
||||||
context: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IntegrationApp {
|
interface IntegrationApp {
|
||||||
name: string;
|
name: string;
|
||||||
siteId?: string;
|
appId?: string;
|
||||||
owner?: string;
|
owner?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
integration: TIntegration;
|
integration: Integration;
|
||||||
integrations: TIntegration[];
|
integrations: Integration[];
|
||||||
setIntegrations: any;
|
setIntegrations: any;
|
||||||
bot: any;
|
bot: any;
|
||||||
setBot: any;
|
setBot: any;
|
||||||
environments: Array<{ name: string; slug: string }>;
|
environments: Array<{ name: string; slug: string }>;
|
||||||
|
handleDeleteIntegration: (args: { integration: Integration }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Integration = ({
|
const IntegrationTile = ({
|
||||||
integration,
|
integration,
|
||||||
integrations,
|
integrations,
|
||||||
bot,
|
bot,
|
||||||
setBot,
|
setBot,
|
||||||
setIntegrations,
|
setIntegrations,
|
||||||
environments = []
|
environments = [],
|
||||||
|
handleDeleteIntegration
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
// set initial environment. This find will only execute when component is mounting
|
// set initial environment. This find will only execute when component is mounting
|
||||||
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
||||||
@@ -57,8 +62,7 @@ const Integration = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
|
const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
|
||||||
const [integrationApp, setIntegrationApp] = useState(''); // integration app name
|
const [integrationApp, setIntegrationApp] = useState(''); // integration app name
|
||||||
const [integrationTarget, setIntegrationTarget] = useState(''); // vercel-specific integration param
|
const [integrationTargetEnvironment, setIntegrationTargetEnvironment] = useState('');
|
||||||
const [integrationContext, setIntegrationContext] = useState(''); // netlify-specific integration param
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadIntegration = async () => {
|
const loadIntegration = async () => {
|
||||||
@@ -66,21 +70,23 @@ const Integration = ({
|
|||||||
const tempApps: [IntegrationApp] = await getIntegrationApps({
|
const tempApps: [IntegrationApp] = await getIntegrationApps({
|
||||||
integrationAuthId: integration.integrationAuth
|
integrationAuthId: integration.integrationAuth
|
||||||
});
|
});
|
||||||
|
|
||||||
setApps(tempApps);
|
setApps(tempApps);
|
||||||
setIntegrationApp(integration.app ? integration.app : tempApps[0].name);
|
setIntegrationApp(integration.app ? integration.app : tempApps[0].name);
|
||||||
|
|
||||||
switch (integration.integration) {
|
switch (integration.integration) {
|
||||||
case 'vercel':
|
case 'vercel':
|
||||||
setIntegrationTarget(
|
setIntegrationTargetEnvironment(
|
||||||
integration?.target
|
integration?.targetEnvironment
|
||||||
? integration.target.charAt(0).toUpperCase() + integration.target.substring(1)
|
? integration.targetEnvironment.charAt(0).toUpperCase() + integration.targetEnvironment.substring(1)
|
||||||
: 'Development'
|
: 'Development'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'netlify':
|
case 'netlify':
|
||||||
setIntegrationContext(
|
setIntegrationTargetEnvironment(
|
||||||
integration?.context ? contextNetlifyMapping[integration.context] : 'Local development'
|
integration?.targetEnvironment
|
||||||
|
? contextNetlifyMapping[integration.targetEnvironment]
|
||||||
|
: 'Local development'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -92,22 +98,30 @@ const Integration = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleStartIntegration = async () => {
|
const handleStartIntegration = async () => {
|
||||||
|
const reformatTargetEnvironment = (targetEnvironment: string) => {
|
||||||
|
switch (integration.integration) {
|
||||||
|
case 'vercel':
|
||||||
|
return targetEnvironment.toLowerCase();
|
||||||
|
case 'netlify':
|
||||||
|
return reverseContextNetlifyMapping[targetEnvironment];
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
||||||
const siteId = siteApp?.siteId ?? null;
|
const appId = siteApp?.appId ?? null;
|
||||||
const owner = siteApp?.owner ?? null;
|
const owner = siteApp?.owner ?? null;
|
||||||
|
|
||||||
// return updated integration
|
// return updated integration
|
||||||
const updatedIntegration = await updateIntegration({
|
const updatedIntegration = await updateIntegration({
|
||||||
integrationId: integration._id,
|
integrationId: integration._id,
|
||||||
environment: integrationEnvironment.slug,
|
environment: integrationEnvironment.slug,
|
||||||
app: integrationApp,
|
|
||||||
isActive: true,
|
isActive: true,
|
||||||
target: integrationTarget ? integrationTarget.toLowerCase() : null,
|
app: integrationApp,
|
||||||
context: integrationContext
|
appId,
|
||||||
? reverseContextNetlifyMapping[integrationContext]
|
targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment),
|
||||||
: null,
|
|
||||||
siteId,
|
|
||||||
owner
|
owner
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,30 +133,8 @@ const Integration = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteIntegration = async () => {
|
|
||||||
try {
|
|
||||||
const deletedIntegration = await deleteIntegration({
|
|
||||||
integrationId: integration._id
|
|
||||||
});
|
|
||||||
|
|
||||||
const newIntegrations = integrations.filter((i) => i._id !== deletedIntegration._id);
|
|
||||||
setIntegrations(newIntegrations);
|
|
||||||
|
|
||||||
if (newIntegrations.length < 1) {
|
|
||||||
// case: no integrations left
|
|
||||||
setBot({
|
|
||||||
...bot,
|
|
||||||
isActive: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const renderIntegrationSpecificParams = (integration: TIntegration) => {
|
const renderIntegrationSpecificParams = (integration: Integration) => {
|
||||||
try {
|
try {
|
||||||
switch (integration.integration) {
|
switch (integration.integration) {
|
||||||
case 'vercel':
|
case 'vercel':
|
||||||
@@ -151,8 +143,8 @@ const Integration = ({
|
|||||||
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">ENVIRONMENT</div>
|
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">ENVIRONMENT</div>
|
||||||
<ListBox
|
<ListBox
|
||||||
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
|
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
|
||||||
isSelected={integrationTarget}
|
isSelected={integrationTargetEnvironment}
|
||||||
onChange={setIntegrationTarget}
|
onChange={setIntegrationTargetEnvironment}
|
||||||
isFull
|
isFull
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,8 +159,8 @@ const Integration = ({
|
|||||||
? ['Production', 'Deploy previews', 'Branch deploys', 'Local development']
|
? ['Production', 'Deploy previews', 'Branch deploys', 'Local development']
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
isSelected={integrationContext}
|
isSelected={integrationTargetEnvironment}
|
||||||
onChange={setIntegrationContext}
|
onChange={setIntegrationTargetEnvironment}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -240,7 +232,9 @@ const Integration = ({
|
|||||||
)}
|
)}
|
||||||
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
|
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => handleDeleteIntegration()}
|
onButtonPressed={() => handleDeleteIntegration({
|
||||||
|
integration
|
||||||
|
})}
|
||||||
color="red"
|
color="red"
|
||||||
size="icon-md"
|
size="icon-md"
|
||||||
icon={faX}
|
icon={faX}
|
||||||
@@ -251,4 +245,4 @@ const Integration = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Integration;
|
export default IntegrationTile;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import guidGenerator from '@app/components/utilities/randomId';
|
import IntegrationTile from './Integration';
|
||||||
|
|
||||||
import Integration from './Integration';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
integrations: any;
|
integrations: any;
|
||||||
@@ -8,16 +6,21 @@ interface Props {
|
|||||||
bot: any;
|
bot: any;
|
||||||
setBot: any;
|
setBot: any;
|
||||||
environments: Array<{ name: string; slug: string }>;
|
environments: Array<{ name: string; slug: string }>;
|
||||||
|
handleDeleteIntegration: (args: { integration: Integration }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IntegrationType {
|
interface Integration {
|
||||||
_id: string;
|
_id: string;
|
||||||
app?: string;
|
isActive: boolean;
|
||||||
|
app: string | null;
|
||||||
|
appId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
integration: string;
|
integration: string;
|
||||||
|
targetEnvironment: string;
|
||||||
|
workspace: string;
|
||||||
integrationAuth: string;
|
integrationAuth: string;
|
||||||
isActive: boolean;
|
|
||||||
context: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProjectIntegrationSection = ({
|
const ProjectIntegrationSection = ({
|
||||||
@@ -25,7 +28,8 @@ const ProjectIntegrationSection = ({
|
|||||||
setIntegrations,
|
setIntegrations,
|
||||||
bot,
|
bot,
|
||||||
setBot,
|
setBot,
|
||||||
environments = []
|
environments = [],
|
||||||
|
handleDeleteIntegration
|
||||||
}: Props) =>
|
}: Props) =>
|
||||||
integrations.length > 0 ? (
|
integrations.length > 0 ? (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
@@ -35,20 +39,24 @@ const ProjectIntegrationSection = ({
|
|||||||
Manage integrations with third-party services.
|
Manage integrations with third-party services.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{integrations.map((integration: IntegrationType) => (
|
{integrations.map((integration: Integration) => {
|
||||||
<Integration
|
console.log('IntegrationSection integration: ', integration);
|
||||||
key={guidGenerator()}
|
return (
|
||||||
integration={integration}
|
<IntegrationTile
|
||||||
integrations={integrations}
|
key={`integration-${integration._id.toString()}`}
|
||||||
bot={bot}
|
integration={integration}
|
||||||
setBot={setBot}
|
integrations={integrations}
|
||||||
setIntegrations={setIntegrations}
|
bot={bot}
|
||||||
environments={environments}
|
setBot={setBot}
|
||||||
/>
|
setIntegrations={setIntegrations}
|
||||||
))}
|
environments={environments}
|
||||||
|
handleDeleteIntegration={handleDeleteIntegration}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div />
|
<div />
|
||||||
);
|
);
|
||||||
|
|
||||||
export default ProjectIntegrationSection;
|
export default ProjectIntegrationSection;
|
||||||
@@ -8,7 +8,7 @@ interface BotKey {
|
|||||||
interface Props {
|
interface Props {
|
||||||
botId: string;
|
botId: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
botKey: BotKey;
|
botKey?: BotKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const deleteIntegrationAuth = ({ integrationAuthId }: Props) =>
|
|||||||
}
|
}
|
||||||
}).then(async (res) => {
|
}).then(async (res) => {
|
||||||
if (res && res.status === 200) {
|
if (res && res.status === 200) {
|
||||||
return res;
|
return (await res.json()).integrationAuth;
|
||||||
}
|
}
|
||||||
console.log('Failed to delete an integration authorization');
|
console.log('Failed to delete an integration authorization');
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
integrationAuthId: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This route creates a new integration based on the integration authorization with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.accessToken - id of integration authorization for which to create the integration
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const createIntegration = ({
|
||||||
|
integrationAuthId
|
||||||
|
}: Props) =>
|
||||||
|
SecurityClient.fetchCall('/api/v1/integration', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
integrationAuthId
|
||||||
|
})
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
return (await res.json()).integration;
|
||||||
|
}
|
||||||
|
console.log('Failed to create integration');
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createIntegration;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
workspaceId: string | null;
|
||||||
|
integration: string | undefined;
|
||||||
|
accessToken: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This route creates a new integration authorization for integration [integration]
|
||||||
|
* that requires the user to input their access token manually (e.g. Render). It
|
||||||
|
* saves access token [accessToken] under that integration for workspace with id
|
||||||
|
* [workspaceId].
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace to authorize integration for
|
||||||
|
* @param {String} obj.integration - integration
|
||||||
|
* @param {String} obj.accessToken - access token to save
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const saveIntegrationAccessToken = ({
|
||||||
|
workspaceId,
|
||||||
|
integration,
|
||||||
|
accessToken
|
||||||
|
}: Props) =>
|
||||||
|
SecurityClient.fetchCall(`/api/v1/integration-auth/access-token`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
workspaceId,
|
||||||
|
integration,
|
||||||
|
accessToken
|
||||||
|
})
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
return (await res.json()).integrationAuth;
|
||||||
|
}
|
||||||
|
console.log('Failed to save integration access token');
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default saveIntegrationAccessToken;
|
||||||
@@ -6,32 +6,29 @@ import SecurityClient from '@app/components/utilities/SecurityClient';
|
|||||||
* [environment] to the integration [app] with active state [isActive]
|
* [environment] to the integration [app] with active state [isActive]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.integrationId - id of integration
|
* @param {String} obj.integrationId - id of integration
|
||||||
* @param {String} obj.app - name of app
|
|
||||||
* @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) for Vercel integration
|
* @param {String} obj.environment - project environment to push secrets from
|
||||||
* @param {String} obj.context - (optional) context (environment) for Netlify integration
|
* @param {String} obj.app - name of app
|
||||||
* @param {String} obj.siteId - (optional) app (site_id) for Netlify integration
|
* @param {String} obj.appId - (optional) app ID for integration
|
||||||
|
* @param {String} obj.targetEnvironment - target environment for integration
|
||||||
* @param {String} obj.owner - (optional) owner login of repo for GitHub integration
|
* @param {String} obj.owner - (optional) owner login of repo for GitHub integration
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const updateIntegration = ({
|
const updateIntegration = ({
|
||||||
integrationId,
|
integrationId,
|
||||||
app,
|
|
||||||
environment,
|
|
||||||
isActive,
|
isActive,
|
||||||
target,
|
environment,
|
||||||
context,
|
app,
|
||||||
siteId,
|
appId,
|
||||||
|
targetEnvironment,
|
||||||
owner
|
owner
|
||||||
}: {
|
}: {
|
||||||
integrationId: string;
|
integrationId: string;
|
||||||
app: string;
|
|
||||||
environment: string;
|
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
target: string | null;
|
environment: string;
|
||||||
context: string | null;
|
app: string;
|
||||||
siteId: string | null;
|
appId: string | null;
|
||||||
|
targetEnvironment: string | null;
|
||||||
owner: string | null;
|
owner: string | null;
|
||||||
}) =>
|
}) =>
|
||||||
SecurityClient.fetchCall(`/api/v1/integration/${integrationId}`, {
|
SecurityClient.fetchCall(`/api/v1/integration/${integrationId}`, {
|
||||||
@@ -43,9 +40,8 @@ const updateIntegration = ({
|
|||||||
app,
|
app,
|
||||||
environment,
|
environment,
|
||||||
isActive,
|
isActive,
|
||||||
target,
|
appId,
|
||||||
context,
|
targetEnvironment,
|
||||||
siteId,
|
|
||||||
owner
|
owner
|
||||||
})
|
})
|
||||||
}).then(async (res) => {
|
}).then(async (res) => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useTranslation } from 'next-i18next';
|
|||||||
import frameworkIntegrationOptions from 'public/json/frameworkIntegrations.json';
|
import frameworkIntegrationOptions from 'public/json/frameworkIntegrations.json';
|
||||||
|
|
||||||
import ActivateBotDialog from '@app/components/basic/dialog/ActivateBotDialog';
|
import ActivateBotDialog from '@app/components/basic/dialog/ActivateBotDialog';
|
||||||
|
import IntegrationAccessTokenDialog from '@app/components/basic/dialog/IntegrationAccessTokenDialog';
|
||||||
import CloudIntegrationSection from '@app/components/integrations/CloudIntegrationSection';
|
import CloudIntegrationSection from '@app/components/integrations/CloudIntegrationSection';
|
||||||
import FrameworkIntegrationSection from '@app/components/integrations/FrameworkIntegrationSection';
|
import FrameworkIntegrationSection from '@app/components/integrations/FrameworkIntegrationSection';
|
||||||
import IntegrationSection from '@app/components/integrations/IntegrationSection';
|
import IntegrationSection from '@app/components/integrations/IntegrationSection';
|
||||||
@@ -19,27 +20,63 @@ import {
|
|||||||
} from '../../components/utilities/cryptography/crypto';
|
} from '../../components/utilities/cryptography/crypto';
|
||||||
import getBot from '../api/bot/getBot';
|
import getBot from '../api/bot/getBot';
|
||||||
import setBotActiveStatus from '../api/bot/setBotActiveStatus';
|
import setBotActiveStatus from '../api/bot/setBotActiveStatus';
|
||||||
|
import createIntegration from '../api/integrations/createIntegration';
|
||||||
|
import deleteIntegration from '../api/integrations/DeleteIntegration';
|
||||||
import getIntegrationOptions from '../api/integrations/GetIntegrationOptions';
|
import getIntegrationOptions from '../api/integrations/GetIntegrationOptions';
|
||||||
import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations';
|
import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations';
|
||||||
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
|
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
|
||||||
|
import saveIntegrationAccessToken from '../api/integrations/saveIntegrationAccessToken';
|
||||||
import getAWorkspace from '../api/workspace/getAWorkspace';
|
import getAWorkspace from '../api/workspace/getAWorkspace';
|
||||||
import getLatestFileKey from '../api/workspace/getLatestFileKey';
|
import getLatestFileKey from '../api/workspace/getLatestFileKey';
|
||||||
|
|
||||||
|
interface IntegrationAuth {
|
||||||
|
_id: string;
|
||||||
|
integration: string;
|
||||||
|
workspace: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Integration {
|
||||||
|
_id: string;
|
||||||
|
isActive: boolean;
|
||||||
|
app: string | null;
|
||||||
|
appId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
environment: string;
|
||||||
|
integration: string;
|
||||||
|
targetEnvironment: string;
|
||||||
|
workspace: string;
|
||||||
|
integrationAuth: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntegrationOption {
|
||||||
|
clientId: string;
|
||||||
|
clientSlug?: string; // vercel-integration specific
|
||||||
|
docsLink: string;
|
||||||
|
image: string;
|
||||||
|
isAvailable: boolean;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Integrations() {
|
export default function Integrations() {
|
||||||
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
|
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
|
||||||
const [integrationAuths, setIntegrationAuths] = useState([]);
|
const [integrationAuths, setIntegrationAuths] = useState<IntegrationAuth[]>([]);
|
||||||
const [environments, setEnvironments] = useState<
|
const [environments, setEnvironments] = useState<
|
||||||
{
|
{
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}[]
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
const [integrations, setIntegrations] = useState([]);
|
const [integrations, setIntegrations] = useState<Integration[]>([]);
|
||||||
// TODO: These will have its type when migratiing towards react-query
|
// TODO: These will have its type when migratiing towards react-query
|
||||||
const [bot, setBot] = useState<any>(null);
|
const [bot, setBot] = useState<any>(null);
|
||||||
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
|
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
|
||||||
// const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(true);
|
const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(false);
|
||||||
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null);
|
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState<IntegrationOption | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workspaceId = router.query.id as string;
|
const workspaceId = router.query.id as string;
|
||||||
@@ -72,7 +109,7 @@ export default function Integrations() {
|
|||||||
// get project bot
|
// get project bot
|
||||||
setBot(await getBot({ workspaceId }));
|
setBot(await getBot({ workspaceId }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -118,7 +155,7 @@ export default function Integrations() {
|
|||||||
(
|
(
|
||||||
await setBotActiveStatus({
|
await setBotActiveStatus({
|
||||||
botId: bot._id,
|
botId: bot._id,
|
||||||
isActive: !bot.isActive,
|
isActive: true,
|
||||||
botKey
|
botKey
|
||||||
})
|
})
|
||||||
).bot
|
).bot
|
||||||
@@ -128,9 +165,9 @@ export default function Integrations() {
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start integration for a given integration option [integrationOption]
|
* Handle integration option authorization for a given integration option [integrationOption]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Object} obj.integrationOption - an integration option
|
* @param {Object} obj.integrationOption - an integration option
|
||||||
* @param {String} obj.name
|
* @param {String} obj.name
|
||||||
@@ -138,45 +175,68 @@ export default function Integrations() {
|
|||||||
* @param {String} obj.docsLink
|
* @param {String} obj.docsLink
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const handleIntegrationOption = async ({ integrationOption }: { integrationOption: any }) => {
|
const handleIntegrationOption = async ({
|
||||||
|
integrationOption,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integrationOption: IntegrationOption,
|
||||||
|
accessToken?: string;
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
// generate CSRF token for OAuth2 code-token exchange integrations
|
if (integrationOption.type === 'oauth') {
|
||||||
const state = crypto.randomBytes(16).toString('hex');
|
// integration is of type OAuth
|
||||||
localStorage.setItem('latestCSRFToken', state);
|
|
||||||
|
|
||||||
switch (integrationOption.name) {
|
// generate CSRF token for OAuth2 code-token exchange integrations
|
||||||
case 'Heroku':
|
const state = crypto.randomBytes(16).toString('hex');
|
||||||
window.location.assign(
|
localStorage.setItem('latestCSRFToken', state);
|
||||||
`https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`
|
|
||||||
);
|
switch (integrationOption.slug) {
|
||||||
break;
|
case 'heroku':
|
||||||
case 'Vercel':
|
window.location.assign(
|
||||||
window.location.assign(
|
`https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`
|
||||||
`https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`
|
);
|
||||||
);
|
break;
|
||||||
break;
|
case 'vercel':
|
||||||
case 'Netlify':
|
window.location.assign(
|
||||||
window.location.assign(
|
`https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`
|
||||||
`https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`
|
);
|
||||||
);
|
break;
|
||||||
break;
|
case 'netlify':
|
||||||
case 'GitHub':
|
window.location.assign(
|
||||||
window.location.assign(
|
`https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`
|
||||||
`https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`
|
);
|
||||||
);
|
break;
|
||||||
break;
|
case 'github':
|
||||||
default:
|
window.location.assign(
|
||||||
break;
|
`https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`
|
||||||
// case 'Fly.io':
|
);
|
||||||
// console.log('fly.io');
|
break;
|
||||||
// setIntegrationAccessTokenDialogOpen(true);
|
default:
|
||||||
// break;
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} if (integrationOption.type === 'pat') {
|
||||||
|
// integration is of type personal access token
|
||||||
|
const integrationAuth = await saveIntegrationAccessToken({
|
||||||
|
workspaceId: localStorage.getItem('projectData.id'),
|
||||||
|
integration: integrationOption.slug,
|
||||||
|
accessToken: accessToken ?? ''
|
||||||
|
});
|
||||||
|
|
||||||
|
setIntegrationAuths([...integrationAuths, integrationAuth])
|
||||||
|
|
||||||
|
const integration = await createIntegration({
|
||||||
|
integrationAuthId: integrationAuth._id
|
||||||
|
});
|
||||||
|
|
||||||
|
setIntegrations([...integrations, integration]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open dialog to activate bot if bot is not active.
|
* Open dialog to activate bot if bot is not active.
|
||||||
* Otherwise, start integration [integrationOption]
|
* Otherwise, start integration [integrationOption]
|
||||||
@@ -186,20 +246,94 @@ export default function Integrations() {
|
|||||||
* @param {String} integrationOption.docsLink
|
* @param {String} integrationOption.docsLink
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const integrationOptionPress = (integrationOption: any) => {
|
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
|
||||||
try {
|
try {
|
||||||
if (bot.isActive) {
|
const integrationAuthX = integrationAuths.find((integrationAuth) => integrationAuth.integration === integrationOption.slug);
|
||||||
// case: bot is active -> proceed with integration
|
|
||||||
|
if (!integrationAuthX) {
|
||||||
|
// case: integration has not been authorized before
|
||||||
|
|
||||||
|
if (integrationOption.type === 'pat') {
|
||||||
|
// case: integration requires user to input their personal access token for that integration
|
||||||
|
setIntegrationAccessTokenDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// case: integration does not require user to input their personal access token (i.e. it's an OAuth2 integration)
|
||||||
handleIntegrationOption({ integrationOption });
|
handleIntegrationOption({ integrationOption });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// case: bot is not active -> open modal to activate bot
|
// case: integration has been authorized before
|
||||||
setIsActivateBotDialogOpen(true);
|
// -> create new integration
|
||||||
|
const integration = await createIntegration({
|
||||||
|
integrationAuthId: integrationAuthX._id
|
||||||
|
});
|
||||||
|
|
||||||
|
setIntegrations([...integrations, integration]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle deleting integration authorization [integrationAuth] and corresponding integrations from state where applicable
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IntegrationAuth} obj.integrationAuth - integrationAuth to delete
|
||||||
|
*/
|
||||||
|
const handleDeleteIntegrationAuth = async ({ integrationAuth: deletedIntegrationAuth }: { integrationAuth: IntegrationAuth }) => {
|
||||||
|
try {
|
||||||
|
const newIntegrations = integrations.filter((integration) => integration.integrationAuth !== deletedIntegrationAuth._id);
|
||||||
|
setIntegrationAuths(integrationAuths.filter((integrationAuth) => integrationAuth._id !== deletedIntegrationAuth._id));
|
||||||
|
setIntegrations(newIntegrations);
|
||||||
|
|
||||||
|
// handle updating bot
|
||||||
|
if (newIntegrations.length < 1) {
|
||||||
|
// case: no integrations left
|
||||||
|
setBot(
|
||||||
|
(
|
||||||
|
await setBotActiveStatus({
|
||||||
|
botId: bot._id,
|
||||||
|
isActive: false
|
||||||
|
})
|
||||||
|
).bot
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle deleting integration [integration]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Integration} obj.integration - integration to delete
|
||||||
|
*/
|
||||||
|
const handleDeleteIntegration = async ({ integration }: { integration: Integration }) => {
|
||||||
|
try {
|
||||||
|
const deletedIntegration = await deleteIntegration({
|
||||||
|
integrationId: integration._id
|
||||||
|
});
|
||||||
|
|
||||||
|
const newIntegrations = integrations.filter((i) => i._id !== deletedIntegration._id);
|
||||||
|
setIntegrations(newIntegrations);
|
||||||
|
|
||||||
|
// handle updating bot
|
||||||
|
if (newIntegrations.length < 1) {
|
||||||
|
// case: no integrations left
|
||||||
|
setBot(
|
||||||
|
(
|
||||||
|
await setBotActiveStatus({
|
||||||
|
botId: bot._id,
|
||||||
|
isActive: false
|
||||||
|
})
|
||||||
|
).bot
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||||
@@ -217,28 +351,37 @@ export default function Integrations() {
|
|||||||
closeModal={() => setIsActivateBotDialogOpen(false)}
|
closeModal={() => setIsActivateBotDialogOpen(false)}
|
||||||
selectedIntegrationOption={selectedIntegrationOption}
|
selectedIntegrationOption={selectedIntegrationOption}
|
||||||
handleBotActivate={handleBotActivate}
|
handleBotActivate={handleBotActivate}
|
||||||
handleIntegrationOption={handleIntegrationOption}
|
integrationOptionPress={integrationOptionPress}
|
||||||
/>
|
/>
|
||||||
{/* <IntegrationAccessTokenDialog
|
<IntegrationAccessTokenDialog
|
||||||
isOpen={isIntegrationAccessTokenDialogOpen}
|
isOpen={isIntegrationAccessTokenDialogOpen}
|
||||||
closeModal={() => setIntegrationAccessTokenDialogOpen(false)}
|
closeModal={() => setIntegrationAccessTokenDialogOpen(false)}
|
||||||
selectedIntegrationOption={selectedIntegrationOption}
|
selectedIntegrationOption={selectedIntegrationOption}
|
||||||
handleBotActivate={handleBotActivate}
|
|
||||||
handleIntegrationOption={handleIntegrationOption}
|
handleIntegrationOption={handleIntegrationOption}
|
||||||
/> */}
|
|
||||||
|
/>
|
||||||
<IntegrationSection
|
<IntegrationSection
|
||||||
integrations={integrations}
|
integrations={integrations}
|
||||||
setIntegrations={setIntegrations}
|
setIntegrations={setIntegrations}
|
||||||
bot={bot}
|
bot={bot}
|
||||||
setBot={setBot}
|
setBot={setBot}
|
||||||
environments={environments}
|
environments={environments}
|
||||||
|
handleDeleteIntegration={handleDeleteIntegration}
|
||||||
/>
|
/>
|
||||||
{cloudIntegrationOptions.length > 0 && bot ? (
|
{cloudIntegrationOptions.length > 0 && bot ? (
|
||||||
<CloudIntegrationSection
|
<CloudIntegrationSection
|
||||||
cloudIntegrationOptions={cloudIntegrationOptions}
|
cloudIntegrationOptions={cloudIntegrationOptions}
|
||||||
setSelectedIntegrationOption={setSelectedIntegrationOption as any}
|
setSelectedIntegrationOption={setSelectedIntegrationOption as any}
|
||||||
integrationOptionPress={integrationOptionPress as any}
|
integrationOptionPress={(integrationOption: IntegrationOption) => {
|
||||||
|
if (!bot.isActive) {
|
||||||
|
// case: bot is not active -> open modal to activate bot
|
||||||
|
setIsActivateBotDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
integrationOptionPress(integrationOption)
|
||||||
|
}}
|
||||||
integrationAuths={integrationAuths}
|
integrationAuths={integrationAuths}
|
||||||
|
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div />
|
<div />
|
||||||
|
|||||||