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

|
||||

|
||||
|
BIN
docs/images/integrations-flyio-auth.png
Normal file
After Width: | Height: | Size: 350 KiB |
BIN
docs/images/integrations-flyio-dashboard.png
Normal file
After Width: | Height: | Size: 285 KiB |
BIN
docs/images/integrations-flyio-token.png
Normal file
After Width: | Height: | Size: 259 KiB |
BIN
docs/images/integrations-flyio.png
Normal file
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 |
BIN
docs/images/integrations-render-auth.png
Normal file
After Width: | Height: | Size: 351 KiB |
BIN
docs/images/integrations-render-dashboard.png
Normal file
After Width: | Height: | Size: 204 KiB |
BIN
docs/images/integrations-render-token.png
Normal file
After Width: | Height: | Size: 266 KiB |
BIN
docs/images/integrations-render.png
Normal file
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"
|
||||
---
|
||||
|
||||
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"
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: 'Overview'
|
||||
title: "Overview"
|
||||
---
|
||||
|
||||
Integrations allow environment variables to be synced from Infisical into your local development workflow, CI/CD pipelines, and production infrastructure.
|
||||
@ -14,6 +14,8 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
|
||||
| [Heroku](/integrations/cloud/heroku) | Cloud | Available |
|
||||
| [Vercel](/integrations/cloud/vercel) | Cloud | Available |
|
||||
| [Netlify](/integrations/cloud/netlify) | Cloud | Available |
|
||||
| [Render](/integrations/cloud/render) | Cloud | Available |
|
||||
| [Fly.io](/integrations/cloud/flyio) | Cloud | Available |
|
||||
| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available |
|
||||
| [React](/integrations/frameworks/react) | Framework | Available |
|
||||
| [Vue](/integrations/frameworks/vue) | Framework | Available |
|
||||
@ -29,8 +31,6 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
|
||||
| [Flask](/integrations/frameworks/flask) | Framework | Available |
|
||||
| [Laravel](/integrations/frameworks/laravel) | Framework | Available |
|
||||
| [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available |
|
||||
| [Render](/integrations/cloud/render) | Cloud | Coming soon |
|
||||
| [Fly.io](/integrations/cloud/flyio) | Cloud | Coming soon |
|
||||
| AWS | Cloud | Coming soon |
|
||||
| GCP | Cloud | Coming soon |
|
||||
| Azure | Cloud | Coming soon |
|
||||
|
@ -27,7 +27,7 @@
|
||||
"http://localhost:8080"
|
||||
],
|
||||
"auth": {
|
||||
"method": "api-key",
|
||||
"method": "key",
|
||||
"name": "X-API-KEY"
|
||||
}
|
||||
},
|
||||
|
1
frontend/public/images/integrations/Flyio.svg
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
frontend/public/images/integrations/Render.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
@ -4,12 +4,23 @@ import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
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 = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
selectedIntegrationOption: never[] | null;
|
||||
selectedIntegrationOption: IntegrationOption | null;
|
||||
handleBotActivate: () => Promise<void>;
|
||||
handleIntegrationOption: (arg: { integrationOption: never[] }) => void;
|
||||
integrationOptionPress: (integrationOption: IntegrationOption) => void;
|
||||
};
|
||||
|
||||
const ActivateBotDialog = ({
|
||||
@ -17,7 +28,7 @@ const ActivateBotDialog = ({
|
||||
closeModal,
|
||||
selectedIntegrationOption,
|
||||
handleBotActivate,
|
||||
handleIntegrationOption
|
||||
integrationOptionPress
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -28,10 +39,10 @@ const ActivateBotDialog = ({
|
||||
|
||||
// type check
|
||||
if (!selectedIntegrationOption) return;
|
||||
// 2. start integration
|
||||
await handleIntegrationOption({
|
||||
integrationOption: selectedIntegrationOption
|
||||
});
|
||||
|
||||
// 2. start integration or probe for PAT
|
||||
integrationOptionPress(selectedIntegrationOption);
|
||||
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
@ -1,45 +1,60 @@
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import Button from "../buttons/Button";
|
||||
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 = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
selectedIntegrationOption: string;
|
||||
handleBotActivate: () => void;
|
||||
handleIntegrationOption: (arg:{integrationOption:string})=>void;
|
||||
selectedIntegrationOption: IntegrationOption | null
|
||||
handleIntegrationOption: (arg:{
|
||||
integrationOption: IntegrationOption,
|
||||
accessToken?: string;
|
||||
})=>void;
|
||||
};
|
||||
|
||||
const IntegrationAccessTokenDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
selectedIntegrationOption,
|
||||
handleBotActivate,
|
||||
handleIntegrationOption
|
||||
}:Props) => {
|
||||
|
||||
const [accessToken, setAccessToken] = useState('');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const submit = async () => {
|
||||
try {
|
||||
// 1. activate bot
|
||||
await handleBotActivate();
|
||||
|
||||
// 2. start integration
|
||||
await handleIntegrationOption({
|
||||
integrationOption: selectedIntegrationOption
|
||||
});
|
||||
if (selectedIntegrationOption && accessToken !== '') {
|
||||
handleIntegrationOption({
|
||||
integrationOption: selectedIntegrationOption,
|
||||
accessToken
|
||||
});
|
||||
closeModal();
|
||||
setAccessToken('');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
closeModal();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<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
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@ -67,28 +82,30 @@ const IntegrationAccessTokenDialog = ({
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
Grant Infisical access to your secrets
|
||||
{`Enter your ${selectedIntegrationOption?.name} API Key`}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-2">
|
||||
<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>
|
||||
</div>
|
||||
<div className="mt-6 max-w-max">
|
||||
{/* <Button
|
||||
onButtonPressed={submit}
|
||||
color="mineshaft"
|
||||
text="Grant access"
|
||||
size="md"
|
||||
/> */}
|
||||
<InputField
|
||||
label="Access token"
|
||||
onChangeHandler={() => {}}
|
||||
label="API Key"
|
||||
onChangeHandler={setAccessToken}
|
||||
type="varName"
|
||||
value="Hello"
|
||||
value={accessToken}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
color="mineshaft"
|
||||
text="Connect"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
|
@ -1,38 +1,42 @@
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import deleteIntegrationAuth from '../../pages/api/integrations/DeleteIntegrationAuth';
|
||||
|
||||
interface CloudIntegrationOption {
|
||||
interface IntegrationOption {
|
||||
clientId: string;
|
||||
clientSlug?: string; // vercel-integration specific
|
||||
docsLink: string;
|
||||
image: string;
|
||||
isAvailable: boolean;
|
||||
name: string;
|
||||
type: string;
|
||||
clientId: string;
|
||||
docsLink: string;
|
||||
slug: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface IntegrationAuth {
|
||||
_id: string;
|
||||
integration: string;
|
||||
workspace: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
cloudIntegrationOption: CloudIntegrationOption;
|
||||
setSelectedIntegrationOption: (cloudIntegration: CloudIntegrationOption) => void;
|
||||
integrationOptionPress: (cloudIntegrationOption: CloudIntegrationOption) => void;
|
||||
cloudIntegrationOption: IntegrationOption;
|
||||
setSelectedIntegrationOption: (cloudIntegration: IntegrationOption) => void;
|
||||
integrationOptionPress: (cloudIntegrationOption: IntegrationOption) => void;
|
||||
integrationAuths: IntegrationAuth[];
|
||||
handleDeleteIntegrationAuth: (args: { integrationAuth: IntegrationAuth }) => void;
|
||||
}
|
||||
|
||||
const CloudIntegration = ({
|
||||
cloudIntegrationOption,
|
||||
setSelectedIntegrationOption,
|
||||
integrationOptionPress,
|
||||
integrationAuths
|
||||
integrationAuths,
|
||||
handleDeleteIntegrationAuth
|
||||
}: Props) => {
|
||||
const router = useRouter();
|
||||
return integrationAuths ? (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
@ -51,7 +55,7 @@ const CloudIntegration = ({
|
||||
key={cloudIntegrationOption.name}
|
||||
>
|
||||
<Image
|
||||
src={`/images/integrations/${cloudIntegrationOption.name}.png`}
|
||||
src={`/images/integrations/${cloudIntegrationOption.image}`}
|
||||
height={70}
|
||||
width={70}
|
||||
alt="integration logo"
|
||||
@ -71,24 +75,26 @@ const CloudIntegration = ({
|
||||
{cloudIntegrationOption.isAvailable &&
|
||||
integrationAuths
|
||||
.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
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(event) => {
|
||||
onClick={async (event) => {
|
||||
event.stopPropagation();
|
||||
deleteIntegrationAuth({
|
||||
const deletedIntegrationAuth = await deleteIntegrationAuth({
|
||||
integrationAuthId: integrationAuths
|
||||
.filter(
|
||||
(authorization) =>
|
||||
authorization.integration === cloudIntegrationOption.name.toLowerCase()
|
||||
authorization.integration === cloudIntegrationOption.slug
|
||||
)
|
||||
.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"
|
||||
>
|
||||
|
@ -2,20 +2,31 @@ import { useTranslation } from 'next-i18next';
|
||||
|
||||
import CloudIntegration from './CloudIntegration';
|
||||
|
||||
interface CloudIntegrationOption {
|
||||
interface IntegrationOption {
|
||||
clientId: string;
|
||||
clientSlug?: string; // vercel-integration specific
|
||||
docsLink: string;
|
||||
image: string;
|
||||
isAvailable: boolean;
|
||||
name: string;
|
||||
type: string;
|
||||
clientId: string;
|
||||
docsLink: string;
|
||||
slug: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface IntegrationAuth {
|
||||
_id: string;
|
||||
integration: string;
|
||||
workspace: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
cloudIntegrationOptions: CloudIntegrationOption[];
|
||||
cloudIntegrationOptions: IntegrationOption[];
|
||||
setSelectedIntegrationOption: () => void;
|
||||
integrationOptionPress: () => void;
|
||||
integrationAuths: any;
|
||||
integrationOptionPress: (integrationOption: IntegrationOption) => void;
|
||||
integrationAuths: IntegrationAuth[];
|
||||
handleDeleteIntegrationAuth: (args: { integrationAuth: IntegrationAuth }) => void;
|
||||
}
|
||||
|
||||
const CloudIntegrationSection = ({
|
||||
@ -23,6 +34,7 @@ const CloudIntegrationSection = ({
|
||||
setSelectedIntegrationOption,
|
||||
integrationOptionPress,
|
||||
integrationAuths,
|
||||
handleDeleteIntegrationAuth
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -45,6 +57,7 @@ const CloudIntegrationSection = ({
|
||||
setSelectedIntegrationOption={setSelectedIntegrationOption}
|
||||
integrationOptionPress={integrationOptionPress}
|
||||
integrationAuths={integrationAuths}
|
||||
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
|
||||
key={`cloud-integration-${cloudIntegrationOption.slug}`}
|
||||
/>
|
||||
))}
|
||||
|
@ -13,29 +13,45 @@ import deleteIntegration from '../../pages/api/integrations/DeleteIntegration';
|
||||
import getIntegrationApps from '../../pages/api/integrations/GetIntegrationApps';
|
||||
import updateIntegration from '../../pages/api/integrations/updateIntegration';
|
||||
|
||||
interface TIntegration {
|
||||
interface Integration {
|
||||
_id: string;
|
||||
app?: string;
|
||||
target?: string;
|
||||
isActive: boolean;
|
||||
app: string | null;
|
||||
appId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
environment: string;
|
||||
integration: string;
|
||||
targetEnvironment: string;
|
||||
workspace: string;
|
||||
integrationAuth: string;
|
||||
isActive: boolean;
|
||||
context: string;
|
||||
}
|
||||
|
||||
interface IntegrationApp {
|
||||
name: string;
|
||||
siteId?: string;
|
||||
appId?: string;
|
||||
owner?: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
integration: TIntegration;
|
||||
integration: Integration;
|
||||
integrations: Integration[];
|
||||
setIntegrations: any;
|
||||
bot: any;
|
||||
setBot: any;
|
||||
environments: Array<{ name: string; slug: string }>;
|
||||
handleDeleteIntegration: (args: { integration: Integration }) => void;
|
||||
};
|
||||
|
||||
const Integration = ({ integration, environments = [] }: Props) => {
|
||||
const IntegrationTile = ({
|
||||
integration,
|
||||
integrations,
|
||||
bot,
|
||||
setBot,
|
||||
setIntegrations,
|
||||
environments = [],
|
||||
handleDeleteIntegration
|
||||
}: Props) => {
|
||||
// set initial environment. This find will only execute when component is mounting
|
||||
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
||||
environments.find(({ slug }) => slug === integration.environment) || {
|
||||
@ -46,8 +62,7 @@ const Integration = ({ integration, environments = [] }: Props) => {
|
||||
const router = useRouter();
|
||||
const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
|
||||
const [integrationApp, setIntegrationApp] = useState(''); // integration app name
|
||||
const [integrationTarget, setIntegrationTarget] = useState(''); // vercel-specific integration param
|
||||
const [integrationContext, setIntegrationContext] = useState(''); // netlify-specific integration param
|
||||
const [integrationTargetEnvironment, setIntegrationTargetEnvironment] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const loadIntegration = async () => {
|
||||
@ -55,21 +70,23 @@ const Integration = ({ integration, environments = [] }: Props) => {
|
||||
const tempApps: [IntegrationApp] = await getIntegrationApps({
|
||||
integrationAuthId: integration.integrationAuth
|
||||
});
|
||||
|
||||
|
||||
setApps(tempApps);
|
||||
setIntegrationApp(integration.app ? integration.app : tempApps[0].name);
|
||||
|
||||
switch (integration.integration) {
|
||||
case 'vercel':
|
||||
setIntegrationTarget(
|
||||
integration?.target
|
||||
? integration.target.charAt(0).toUpperCase() + integration.target.substring(1)
|
||||
: 'Development'
|
||||
setIntegrationTargetEnvironment(
|
||||
integration?.targetEnvironment
|
||||
? integration.targetEnvironment.charAt(0).toUpperCase() + integration.targetEnvironment.substring(1)
|
||||
: 'Development'
|
||||
);
|
||||
break;
|
||||
case 'netlify':
|
||||
setIntegrationContext(
|
||||
integration?.context ? contextNetlifyMapping[integration.context] : 'Local development'
|
||||
setIntegrationTargetEnvironment(
|
||||
integration?.targetEnvironment
|
||||
? contextNetlifyMapping[integration.targetEnvironment]
|
||||
: 'Local development'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
@ -79,9 +96,45 @@ const Integration = ({ integration, environments = [] }: Props) => {
|
||||
|
||||
loadIntegration();
|
||||
}, []);
|
||||
|
||||
const handleStartIntegration = async () => {
|
||||
const reformatTargetEnvironment = (targetEnvironment: string) => {
|
||||
switch (integration.integration) {
|
||||
case 'vercel':
|
||||
return targetEnvironment.toLowerCase();
|
||||
case 'netlify':
|
||||
return reverseContextNetlifyMapping[targetEnvironment];
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
||||
const appId = siteApp?.appId ?? null;
|
||||
const owner = siteApp?.owner ?? null;
|
||||
|
||||
// return updated integration
|
||||
const updatedIntegration = await updateIntegration({
|
||||
integrationId: integration._id,
|
||||
environment: integrationEnvironment.slug,
|
||||
isActive: true,
|
||||
app: integrationApp,
|
||||
appId,
|
||||
targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment),
|
||||
owner
|
||||
});
|
||||
|
||||
setIntegrations(
|
||||
integrations.map((i) => i._id === updatedIntegration._id ? updatedIntegration : i)
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const renderIntegrationSpecificParams = (integration: TIntegration) => {
|
||||
const renderIntegrationSpecificParams = (integration: Integration) => {
|
||||
try {
|
||||
switch (integration.integration) {
|
||||
case 'vercel':
|
||||
@ -90,8 +143,8 @@ const Integration = ({ integration, environments = [] }: Props) => {
|
||||
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">ENVIRONMENT</div>
|
||||
<ListBox
|
||||
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
|
||||
isSelected={integrationTarget}
|
||||
onChange={setIntegrationTarget}
|
||||
isSelected={integrationTargetEnvironment}
|
||||
onChange={setIntegrationTargetEnvironment}
|
||||
isFull
|
||||
/>
|
||||
</div>
|
||||
@ -106,8 +159,8 @@ const Integration = ({ integration, environments = [] }: Props) => {
|
||||
? ['Production', 'Deploy previews', 'Branch deploys', 'Local development']
|
||||
: null
|
||||
}
|
||||
isSelected={integrationContext}
|
||||
onChange={setIntegrationContext}
|
||||
isSelected={integrationTargetEnvironment}
|
||||
onChange={setIntegrationTargetEnvironment}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -172,38 +225,16 @@ const Integration = ({ integration, environments = [] }: Props) => {
|
||||
) : (
|
||||
<Button
|
||||
text="Start Integration"
|
||||
onButtonPressed={async () => {
|
||||
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
||||
const siteId = siteApp?.siteId ?? null;
|
||||
const owner = siteApp?.owner ?? null;
|
||||
|
||||
await updateIntegration({
|
||||
integrationId: integration._id,
|
||||
environment: integrationEnvironment.slug,
|
||||
app: integrationApp,
|
||||
isActive: true,
|
||||
target: integrationTarget ? integrationTarget.toLowerCase() : null,
|
||||
context: integrationContext
|
||||
? reverseContextNetlifyMapping[integrationContext]
|
||||
: null,
|
||||
siteId,
|
||||
owner
|
||||
});
|
||||
|
||||
router.reload();
|
||||
}}
|
||||
onButtonPressed={() => handleStartIntegration()}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
|
||||
<Button
|
||||
onButtonPressed={async () => {
|
||||
await deleteIntegration({
|
||||
integrationId: integration._id
|
||||
});
|
||||
router.reload();
|
||||
}}
|
||||
onButtonPressed={() => handleDeleteIntegration({
|
||||
integration
|
||||
})}
|
||||
color="red"
|
||||
size="icon-md"
|
||||
icon={faX}
|
||||
@ -214,4 +245,4 @@ const Integration = ({ integration, environments = [] }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Integration;
|
||||
export default IntegrationTile;
|
||||
|
@ -1,37 +1,62 @@
|
||||
import guidGenerator from '@app/components/utilities/randomId';
|
||||
|
||||
import Integration from './Integration';
|
||||
import IntegrationTile from './Integration';
|
||||
|
||||
interface Props {
|
||||
integrations: any;
|
||||
setIntegrations: any;
|
||||
bot: any;
|
||||
setBot: any;
|
||||
environments: Array<{ name: string; slug: string }>;
|
||||
handleDeleteIntegration: (args: { integration: Integration }) => void;
|
||||
}
|
||||
|
||||
interface IntegrationType {
|
||||
interface Integration {
|
||||
_id: string;
|
||||
app?: string;
|
||||
isActive: boolean;
|
||||
app: string | null;
|
||||
appId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
environment: string;
|
||||
integration: string;
|
||||
targetEnvironment: string;
|
||||
workspace: string;
|
||||
integrationAuth: string;
|
||||
isActive: boolean;
|
||||
context: string;
|
||||
}
|
||||
|
||||
const ProjectIntegrationSection = ({ integrations, environments = [] }: Props) =>
|
||||
const ProjectIntegrationSection = ({
|
||||
integrations,
|
||||
setIntegrations,
|
||||
bot,
|
||||
setBot,
|
||||
environments = [],
|
||||
handleDeleteIntegration
|
||||
}: Props) =>
|
||||
integrations.length > 0 ? (
|
||||
<div className="mb-12">
|
||||
<div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2">
|
||||
<h1 className="font-semibold text-3xl">Current Integrations</h1>
|
||||
<p className="text-base text-gray-400">
|
||||
Manage your integrations of Infisical with third-party services.
|
||||
Manage integrations with third-party services.
|
||||
</p>
|
||||
</div>
|
||||
{integrations.map((integration: IntegrationType) => (
|
||||
<Integration key={guidGenerator()} integration={integration} environments={environments} />
|
||||
))}
|
||||
{integrations.map((integration: Integration) => {
|
||||
console.log('IntegrationSection integration: ', integration);
|
||||
return (
|
||||
<IntegrationTile
|
||||
key={`integration-${integration._id.toString()}`}
|
||||
integration={integration}
|
||||
integrations={integrations}
|
||||
bot={bot}
|
||||
setBot={setBot}
|
||||
setIntegrations={setIntegrations}
|
||||
environments={environments}
|
||||
handleDeleteIntegration={handleDeleteIntegration}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
|
||||
export default ProjectIntegrationSection;
|
||||
export default ProjectIntegrationSection;
|
@ -8,7 +8,7 @@ interface BotKey {
|
||||
interface Props {
|
||||
botId: string;
|
||||
isActive: boolean;
|
||||
botKey: BotKey;
|
||||
botKey?: BotKey;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,7 +17,7 @@ const deleteIntegration = ({ integrationId }: Props) =>
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).workspace;
|
||||
return (await res.json()).integration;
|
||||
}
|
||||
console.log('Failed to delete an integration');
|
||||
return undefined;
|
||||
|
@ -17,7 +17,7 @@ const deleteIntegrationAuth = ({ integrationAuthId }: Props) =>
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res;
|
||||
return (await res.json()).integrationAuth;
|
||||
}
|
||||
console.log('Failed to delete an integration authorization');
|
||||
return undefined;
|
||||
|
31
frontend/src/pages/api/integrations/createIntegration.ts
Normal file
@ -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]
|
||||
* @param {Object} obj
|
||||
* @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 {String} obj.target - (optional) target (environment) for Vercel integration
|
||||
* @param {String} obj.context - (optional) context (environment) for Netlify integration
|
||||
* @param {String} obj.siteId - (optional) app (site_id) for Netlify integration
|
||||
* @param {String} obj.environment - project environment to push secrets from
|
||||
* @param {String} obj.app - name of app
|
||||
* @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
|
||||
* @returns
|
||||
*/
|
||||
const updateIntegration = ({
|
||||
integrationId,
|
||||
app,
|
||||
environment,
|
||||
isActive,
|
||||
target,
|
||||
context,
|
||||
siteId,
|
||||
environment,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner
|
||||
}: {
|
||||
integrationId: string;
|
||||
app: string;
|
||||
environment: string;
|
||||
isActive: boolean;
|
||||
target: string | null;
|
||||
context: string | null;
|
||||
siteId: string | null;
|
||||
environment: string;
|
||||
app: string;
|
||||
appId: string | null;
|
||||
targetEnvironment: string | null;
|
||||
owner: string | null;
|
||||
}) =>
|
||||
SecurityClient.fetchCall(`/api/v1/integration/${integrationId}`, {
|
||||
@ -43,14 +40,13 @@ const updateIntegration = ({
|
||||
app,
|
||||
environment,
|
||||
isActive,
|
||||
target,
|
||||
context,
|
||||
siteId,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res;
|
||||
return (await res.json()).integration;
|
||||
}
|
||||
console.log('Failed to start an integration');
|
||||
return undefined;
|
||||
|
@ -7,6 +7,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import frameworkIntegrationOptions from 'public/json/frameworkIntegrations.json';
|
||||
|
||||
import ActivateBotDialog from '@app/components/basic/dialog/ActivateBotDialog';
|
||||
import IntegrationAccessTokenDialog from '@app/components/basic/dialog/IntegrationAccessTokenDialog';
|
||||
import CloudIntegrationSection from '@app/components/integrations/CloudIntegrationSection';
|
||||
import FrameworkIntegrationSection from '@app/components/integrations/FrameworkIntegrationSection';
|
||||
import IntegrationSection from '@app/components/integrations/IntegrationSection';
|
||||
@ -19,27 +20,63 @@ import {
|
||||
} from '../../components/utilities/cryptography/crypto';
|
||||
import getBot from '../api/bot/getBot';
|
||||
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 getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations';
|
||||
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
|
||||
import saveIntegrationAccessToken from '../api/integrations/saveIntegrationAccessToken';
|
||||
import getAWorkspace from '../api/workspace/getAWorkspace';
|
||||
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() {
|
||||
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
|
||||
const [integrationAuths, setIntegrationAuths] = useState([]);
|
||||
const [integrationAuths, setIntegrationAuths] = useState<IntegrationAuth[]>([]);
|
||||
const [environments, setEnvironments] = useState<
|
||||
{
|
||||
name: string;
|
||||
slug: string;
|
||||
}[]
|
||||
>([]);
|
||||
const [integrations, setIntegrations] = useState([]);
|
||||
const [integrations, setIntegrations] = useState<Integration[]>([]);
|
||||
// TODO: These will have its type when migratiing towards react-query
|
||||
const [bot, setBot] = useState<any>(null);
|
||||
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
|
||||
// const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(true);
|
||||
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null);
|
||||
const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(false);
|
||||
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState<IntegrationOption | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const workspaceId = router.query.id as string;
|
||||
@ -72,7 +109,7 @@ export default function Integrations() {
|
||||
// get project bot
|
||||
setBot(await getBot({ workspaceId }));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
@ -118,7 +155,7 @@ export default function Integrations() {
|
||||
(
|
||||
await setBotActiveStatus({
|
||||
botId: bot._id,
|
||||
isActive: !bot.isActive,
|
||||
isActive: true,
|
||||
botKey
|
||||
})
|
||||
).bot
|
||||
@ -128,9 +165,9 @@ export default function Integrations() {
|
||||
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.integrationOption - an integration option
|
||||
* @param {String} obj.name
|
||||
@ -138,45 +175,68 @@ export default function Integrations() {
|
||||
* @param {String} obj.docsLink
|
||||
* @returns
|
||||
*/
|
||||
const handleIntegrationOption = async ({ integrationOption }: { integrationOption: any }) => {
|
||||
const handleIntegrationOption = async ({
|
||||
integrationOption,
|
||||
accessToken
|
||||
}: {
|
||||
integrationOption: IntegrationOption,
|
||||
accessToken?: string;
|
||||
}) => {
|
||||
try {
|
||||
// generate CSRF token for OAuth2 code-token exchange integrations
|
||||
const state = crypto.randomBytes(16).toString('hex');
|
||||
localStorage.setItem('latestCSRFToken', state);
|
||||
if (integrationOption.type === 'oauth') {
|
||||
// integration is of type OAuth
|
||||
|
||||
switch (integrationOption.name) {
|
||||
case 'Heroku':
|
||||
window.location.assign(
|
||||
`https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`
|
||||
);
|
||||
break;
|
||||
case 'Vercel':
|
||||
window.location.assign(
|
||||
`https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`
|
||||
);
|
||||
break;
|
||||
case 'Netlify':
|
||||
window.location.assign(
|
||||
`https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`
|
||||
);
|
||||
break;
|
||||
case 'GitHub':
|
||||
window.location.assign(
|
||||
`https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
// case 'Fly.io':
|
||||
// console.log('fly.io');
|
||||
// setIntegrationAccessTokenDialogOpen(true);
|
||||
// break;
|
||||
// generate CSRF token for OAuth2 code-token exchange integrations
|
||||
const state = crypto.randomBytes(16).toString('hex');
|
||||
localStorage.setItem('latestCSRFToken', state);
|
||||
|
||||
switch (integrationOption.slug) {
|
||||
case 'heroku':
|
||||
window.location.assign(
|
||||
`https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`
|
||||
);
|
||||
break;
|
||||
case 'vercel':
|
||||
window.location.assign(
|
||||
`https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`
|
||||
);
|
||||
break;
|
||||
case 'netlify':
|
||||
window.location.assign(
|
||||
`https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`
|
||||
);
|
||||
break;
|
||||
case 'github':
|
||||
window.location.assign(
|
||||
`https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`
|
||||
);
|
||||
break;
|
||||
default:
|
||||
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) {
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Open dialog to activate bot if bot is not active.
|
||||
* Otherwise, start integration [integrationOption]
|
||||
@ -186,20 +246,94 @@ export default function Integrations() {
|
||||
* @param {String} integrationOption.docsLink
|
||||
* @returns
|
||||
*/
|
||||
const integrationOptionPress = (integrationOption: any) => {
|
||||
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
|
||||
try {
|
||||
if (bot.isActive) {
|
||||
// case: bot is active -> proceed with integration
|
||||
const integrationAuthX = integrationAuths.find((integrationAuth) => integrationAuth.integration === integrationOption.slug);
|
||||
|
||||
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 });
|
||||
return;
|
||||
}
|
||||
|
||||
// case: bot is not active -> open modal to activate bot
|
||||
setIsActivateBotDialogOpen(true);
|
||||
|
||||
// case: integration has been authorized before
|
||||
// -> create new integration
|
||||
const integration = await createIntegration({
|
||||
integrationAuthId: integrationAuthX._id
|
||||
});
|
||||
|
||||
setIntegrations([...integrations, integration]);
|
||||
} catch (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 (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
@ -217,22 +351,37 @@ export default function Integrations() {
|
||||
closeModal={() => setIsActivateBotDialogOpen(false)}
|
||||
selectedIntegrationOption={selectedIntegrationOption}
|
||||
handleBotActivate={handleBotActivate}
|
||||
handleIntegrationOption={handleIntegrationOption}
|
||||
integrationOptionPress={integrationOptionPress}
|
||||
/>
|
||||
{/* <IntegrationAccessTokenDialog
|
||||
<IntegrationAccessTokenDialog
|
||||
isOpen={isIntegrationAccessTokenDialogOpen}
|
||||
closeModal={() => setIntegrationAccessTokenDialogOpen(false)}
|
||||
selectedIntegrationOption={selectedIntegrationOption}
|
||||
handleBotActivate={handleBotActivate}
|
||||
handleIntegrationOption={handleIntegrationOption}
|
||||
/> */}
|
||||
<IntegrationSection integrations={integrations} environments={environments} />
|
||||
|
||||
/>
|
||||
<IntegrationSection
|
||||
integrations={integrations}
|
||||
setIntegrations={setIntegrations}
|
||||
bot={bot}
|
||||
setBot={setBot}
|
||||
environments={environments}
|
||||
handleDeleteIntegration={handleDeleteIntegration}
|
||||
/>
|
||||
{cloudIntegrationOptions.length > 0 && bot ? (
|
||||
<CloudIntegrationSection
|
||||
cloudIntegrationOptions={cloudIntegrationOptions}
|
||||
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}
|
||||
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
|