Merge pull request #650 from akhilmhdh/feat/integrations-page

Feat/integrations page
This commit is contained in:
BlackMagiq
2023-06-17 10:06:30 +02:00
committed by GitHub
30 changed files with 1031 additions and 639 deletions
@@ -1,10 +1,11 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import { Integration } from "../../models";
Integration import { EventService } from "../../services";
} from '../../models'; import { eventPushSecrets } from "../../events";
import { EventService } from '../../services'; import Folder from "../../models/folder";
import { eventPushSecrets } from '../../events'; import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
/** /**
* Create/initialize an (empty) integration for integration authorization * Create/initialize an (empty) integration for integration authorization
@@ -25,9 +26,24 @@ export const createIntegration = async (req: Request, res: Response) => {
targetServiceId, targetServiceId,
owner, owner,
path, path,
region region,
secretPath,
} = req.body; } = req.body;
const folders = await Folder.findOne({
workspace: req.integrationAuth.workspace._id,
environment: sourceEnvironment,
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Path for service token does not exist",
});
}
}
// TODO: validate [sourceEnvironment] and [targetEnvironment] // TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token // initialize new integration after saving integration access token
@@ -44,17 +60,18 @@ export const createIntegration = async (req: Request, res: Response) => {
owner, owner,
path, path,
region, region,
secretPath,
integration: req.integrationAuth.integration, integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId) integrationAuth: new Types.ObjectId(integrationAuthId),
}).save(); }).save();
if (integration) { if (integration) {
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: integration.workspace, workspaceId: integration.workspace,
environment: sourceEnvironment environment: sourceEnvironment,
}) }),
}); });
} }
@@ -70,7 +87,6 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateIntegration = async (req: Request, res: Response) => { export const updateIntegration = async (req: Request, res: Response) => {
// TODO: add integration-specific validation to ensure that each // TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration] // integration has the correct fields populated in [Integration]
@@ -81,8 +97,23 @@ export const updateIntegration = async (req: Request, res: Response) => {
appId, appId,
targetEnvironment, targetEnvironment,
owner, // github-specific integration param owner, // github-specific integration param
secretPath,
} = req.body; } = req.body;
const folders = await Folder.findOne({
workspace: req.integration.workspace,
environment,
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Path for service token does not exist",
});
}
}
const integration = await Integration.findOneAndUpdate( const integration = await Integration.findOneAndUpdate(
{ {
_id: req.integration._id, _id: req.integration._id,
@@ -94,6 +125,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
appId, appId,
targetEnvironment, targetEnvironment,
owner, owner,
secretPath,
}, },
{ {
new: true, new: true,
@@ -105,7 +137,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: integration.workspace, workspaceId: integration.workspace,
environment environment,
}), }),
}); });
} }
+59 -39
View File
@@ -1,29 +1,21 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import { Bot, BotKey, Secret, ISecret, IUser } from "../models";
Bot,
BotKey,
Secret,
ISecret,
IUser
} from "../models";
import { import {
generateKeyPair, generateKeyPair,
encryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8,
decryptAsymmetric decryptAsymmetric,
} from '../utils/crypto'; } from "../utils/crypto";
import { import {
SECRET_SHARED, SECRET_SHARED,
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64 ENCODING_SCHEME_BASE64,
} from "../variables"; } from "../variables";
import { import { getEncryptionKey, getRootEncryptionKey, client } from "../config";
getEncryptionKey,
getRootEncryptionKey,
client
} from "../config";
import { InternalServerError } from "../utils/errors"; import { InternalServerError } from "../utils/errors";
import Folder from "../models/folder";
import { getFolderByPath } from "../services/FolderService";
/** /**
* Create an inactive bot with name [name] for workspace with id [workspaceId] * Create an inactive bot with name [name] for workspace with id [workspaceId]
@@ -40,15 +32,14 @@ export const createBot = async ({
}) => { }) => {
const encryptionKey = await getEncryptionKey(); const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey();
const { publicKey, privateKey } = generateKeyPair(); const { publicKey, privateKey } = generateKeyPair();
if (rootEncryptionKey) { if (rootEncryptionKey) {
const { const { ciphertext, iv, tag } = client.encryptSymmetric(
ciphertext, privateKey,
iv, rootEncryptionKey
tag );
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
return await new Bot({ return await new Bot({
name, name,
@@ -59,9 +50,8 @@ export const createBot = async ({
iv, iv,
tag, tag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64 keyEncoding: ENCODING_SCHEME_BASE64,
}).save(); }).save();
} else if (encryptionKey) { } else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey, plaintext: privateKey,
@@ -77,12 +67,12 @@ export const createBot = async ({
iv, iv,
tag, tag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8,
}).save(); }).save();
} }
throw InternalServerError({ throw InternalServerError({
message: 'Failed to create new bot due to missing encryption key' message: "Failed to create new bot due to missing encryption key",
}); });
}; };
@@ -92,11 +82,11 @@ export const createBot = async ({
*/ */
export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => { export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
const botKey = await BotKey.exists({ const botKey = await BotKey.exists({
workspace: workspaceId workspace: workspaceId,
}); });
return botKey ? false : true; return botKey ? false : true;
} };
/** /**
* Return decrypted secrets for workspace with id [workspaceId] * Return decrypted secrets for workspace with id [workspaceId]
@@ -108,16 +98,38 @@ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
export const getSecretsBotHelper = async ({ export const getSecretsBotHelper = async ({
workspaceId, workspaceId,
environment, environment,
secretPath,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
secretPath: string;
}) => { }) => {
const content = {} as any; const content = {} as any;
const key = await getKey({ workspaceId: workspaceId }); const key = await getKey({ workspaceId: workspaceId });
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
});
if (!folders && secretPath !== "/") {
throw InternalServerError({ message: "Folder not found" });
}
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw InternalServerError({ message: "Folder not found" });
}
folderId = folder.id;
}
const secrets = await Secret.find({ const secrets = await Secret.find({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
type: SECRET_SHARED, type: SECRET_SHARED,
folder: folderId,
}); });
secrets.forEach((secret: ISecret) => { secrets.forEach((secret: ISecret) => {
@@ -148,14 +160,17 @@ export const getSecretsBotHelper = async ({
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key * @returns {String} key - decrypted workspace key
*/ */
export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => { export const getKey = async ({
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) => {
const encryptionKey = await getEncryptionKey(); const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey();
const botKey = await BotKey.findOne({ const botKey = await BotKey.findOne({
workspace: workspaceId, workspace: workspaceId,
}) }).populate<{ sender: IUser }>("sender", "publicKey");
.populate<{ sender: IUser }>("sender", "publicKey");
if (!botKey) throw new Error("Failed to find bot key"); if (!botKey) throw new Error("Failed to find bot key");
@@ -168,7 +183,12 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) { if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) {
// case: encoding scheme is base64 // case: encoding scheme is base64
const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag); const privateKeyBot = client.decryptSymmetric(
bot.encryptedPrivateKey,
rootEncryptionKey,
bot.iv,
bot.tag
);
return decryptAsymmetric({ return decryptAsymmetric({
ciphertext: botKey.encryptedKey, ciphertext: botKey.encryptedKey,
@@ -177,15 +197,14 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
privateKey: privateKeyBot, privateKey: privateKeyBot,
}); });
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) { } else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
// case: encoding scheme is utf8 // case: encoding scheme is utf8
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({ const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
ciphertext: bot.encryptedPrivateKey, ciphertext: bot.encryptedPrivateKey,
iv: bot.iv, iv: bot.iv,
tag: bot.tag, tag: bot.tag,
key: encryptionKey key: encryptionKey,
}); });
return decryptAsymmetric({ return decryptAsymmetric({
ciphertext: botKey.encryptedKey, ciphertext: botKey.encryptedKey,
nonce: botKey.nonce, nonce: botKey.nonce,
@@ -195,7 +214,8 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
} }
throw InternalServerError({ throw InternalServerError({
message: "Failed to obtain bot's copy of workspace key needed for bot operations" message:
"Failed to obtain bot's copy of workspace key needed for bot operations",
}); });
}; };
@@ -254,4 +274,4 @@ export const decryptSymmetricHelper = async ({
}); });
return plaintext; return plaintext;
}; };
+278 -245
View File
@@ -1,26 +1,20 @@
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { Bot, Integration, IntegrationAuth } from "../models";
import { exchangeCode, exchangeRefresh, syncSecrets } from "../integrations";
import { BotService } from "../services";
import { import {
Bot, INTEGRATION_VERCEL,
Integration, INTEGRATION_NETLIFY,
IntegrationAuth ALGORITHM_AES_256_GCM,
} from '../models'; ENCODING_SCHEME_UTF8,
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; } from "../variables";
import { BotService } from '../services'; import { UnauthorizedRequestError } from "../utils/errors";
import {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8
} from '../variables';
import {
UnauthorizedRequestError,
} from '../utils/errors';
interface Update { interface Update {
workspace: string; workspace: string;
integration: string; integration: string;
teamId?: string; teamId?: string;
accountId?: string; accountId?: string;
} }
/** /**
@@ -31,78 +25,83 @@ interface Update {
* - Create bot sequence for integration * - Create bot sequence for integration
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @param {String} obj.integration - name of integration * @param {String} obj.integration - name of integration
* @param {String} obj.code - code * @param {String} obj.code - code
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange * @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/ */
export const handleOAuthExchangeHelper = async ({ export const handleOAuthExchangeHelper = async ({
workspaceId, workspaceId,
integration,
code,
environment,
}: {
workspaceId: string;
integration: string;
code: string;
environment: string;
}) => {
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true,
});
if (!bot)
throw new Error("Bot must be enabled for OAuth2 code-token exchange");
// exchange code for access and refresh tokens
const res = await exchangeCode({
integration, integration,
code, code,
environment });
}: {
workspaceId: string; const update: Update = {
integration: string; workspace: workspaceId,
code: string; integration,
environment: string; };
}) => {
const bot = await Bot.findOne({ switch (integration) {
workspace: workspaceId, case INTEGRATION_VERCEL:
isActive: true update.teamId = res.teamId;
break;
case INTEGRATION_NETLIFY:
update.accountId = res.accountId;
break;
}
const integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
workspace: workspaceId,
integration,
},
update,
{
new: true,
upsert: true,
}
);
if (res.refreshToken) {
// case: refresh token returned from exchange
// set integration auth refresh token
await setIntegrationAuthRefreshHelper({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: res.refreshToken,
}); });
}
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
if (res.accessToken) {
// exchange code for access and refresh tokens // case: access token returned from exchange
const res = await exchangeCode({ // set integration auth access token
integration, await setIntegrationAuthAccessHelper({
code integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt,
}); });
}
const update: Update = {
workspace: workspaceId, return integrationAuth;
integration };
}
switch (integration) {
case INTEGRATION_VERCEL:
update.teamId = res.teamId;
break;
case INTEGRATION_NETLIFY:
update.accountId = res.accountId;
break;
}
const integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: workspaceId,
integration
}, update, {
new: true,
upsert: true
});
if (res.refreshToken) {
// case: refresh token returned from exchange
// set integration auth refresh token
await setIntegrationAuthRefreshHelper({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: res.refreshToken
});
}
if (res.accessToken) {
// case: access token returned from exchange
// set integration auth access token
await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt
});
}
return integrationAuth;
}
/** /**
* Sync/push environment variables in workspace with id [workspaceId] to * Sync/push environment variables in workspace with id [workspaceId] to
* all active integrations for that workspace * all active integrations for that workspace
@@ -110,48 +109,54 @@ export const handleOAuthExchangeHelper = async ({
* @param {Object} obj.workspaceId - id of workspace * @param {Object} obj.workspaceId - id of workspace
*/ */
export const syncIntegrationsHelper = async ({ export const syncIntegrationsHelper = async ({
workspaceId, workspaceId,
environment environment,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment?: string; environment?: string;
}) => { }) => {
const integrations = await Integration.find({ const integrations = await Integration.find({
workspace: workspaceId, workspace: workspaceId,
...(environment ? { ...(environment
environment ? {
} : {}), environment,
isActive: true, }
app: { $ne: null } : {}),
isActive: true,
app: { $ne: null },
});
// for each workspace integration, sync/push secrets
// to that integration
for await (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({
// issue here?
workspaceId: integration.workspace,
environment: integration.environment,
secretPath: integration.secretPath,
}); });
// for each workspace integration, sync/push secrets const integrationAuth = await IntegrationAuth.findById(
// to that integration integration.integrationAuth
for await (const integration of integrations) { );
// get workspace, environment (shared) secrets if (!integrationAuth) throw new Error("Failed to find integration auth");
const secrets = await BotService.getSecrets({ // issue here?
workspaceId: integration.workspace,
environment: integration.environment
});
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth); // get integration auth access token
if (!integrationAuth) throw new Error('Failed to find integration auth'); const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth,
// get integration auth access token });
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth
});
// sync secrets to integration // sync secrets to integration
await syncSecrets({ await syncSecrets({
integration, integration,
integrationAuth, integrationAuth,
secrets, secrets,
accessId: access.accessId === undefined ? null : access.accessId, accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken accessToken: access.accessToken,
}); });
} }
} };
/** /**
* Return decrypted refresh token using the bot's copy * Return decrypted refresh token using the bot's copy
@@ -161,22 +166,29 @@ export const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @param {String} refreshToken - decrypted refresh token * @param {String} refreshToken - decrypted refresh token
*/ */
export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { export const getIntegrationAuthRefreshHelper = async ({
const integrationAuth = await IntegrationAuth integrationAuthId,
.findById(integrationAuthId) }: {
.select('+refreshCiphertext +refreshIV +refreshTag'); integrationAuthId: Types.ObjectId;
}) => {
const integrationAuth = await IntegrationAuth.findById(
integrationAuthId
).select("+refreshCiphertext +refreshIV +refreshTag");
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); if (!integrationAuth)
throw UnauthorizedRequestError({
const refreshToken = await BotService.decryptSymmetric({ message: "Failed to locate Integration Authentication credentials",
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string
}); });
return refreshToken; const refreshToken = await BotService.decryptSymmetric({
} workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string,
});
return refreshToken;
};
/** /**
* Return decrypted access token using the bot's copy * Return decrypted access token using the bot's copy
@@ -186,50 +198,65 @@ export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { i
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token * @returns {String} accessToken - decrypted access token
*/ */
export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { export const getIntegrationAuthAccessHelper = async ({
let accessId; integrationAuthId,
let accessToken; }: {
const integrationAuth = await IntegrationAuth integrationAuthId: Types.ObjectId;
.findById(integrationAuthId) }) => {
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag'); let accessId;
let accessToken;
const integrationAuth = await IntegrationAuth.findById(
integrationAuthId
).select(
"workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag"
);
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); if (!integrationAuth)
throw UnauthorizedRequestError({
accessToken = await BotService.decryptSymmetric({ message: "Failed to locate Integration Authentication credentials",
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string
}); });
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { accessToken = await BotService.decryptSymmetric({
// there is a access token expiration date workspaceId: integrationAuth.workspace,
// and refresh token to exchange with the OAuth2 server ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
if (integrationAuth.accessExpiresAt < new Date()) { tag: integrationAuth.accessTag as string,
// access token is expired });
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
accessToken = await exchangeRefresh({ if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
integrationAuth, // there is a access token expiration date
refreshToken // and refresh token to exchange with the OAuth2 server
});
} if (integrationAuth.accessExpiresAt < new Date()) {
// access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({
integrationAuthId,
});
accessToken = await exchangeRefresh({
integrationAuth,
refreshToken,
});
} }
}
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
accessId = await BotService.decryptSymmetric({ if (
workspaceId: integrationAuth.workspace, integrationAuth?.accessIdCiphertext &&
ciphertext: integrationAuth.accessIdCiphertext as string, integrationAuth?.accessIdIV &&
iv: integrationAuth.accessIdIV as string, integrationAuth?.accessIdTag
tag: integrationAuth.accessIdTag as string ) {
}); accessId = await BotService.decryptSymmetric({
} workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessIdCiphertext as string,
return ({ iv: integrationAuth.accessIdIV as string,
accessId, tag: integrationAuth.accessIdTag as string,
accessToken
}); });
} }
return {
accessId,
accessToken,
};
};
/** /**
* Encrypt refresh token [refreshToken] using the bot's copy * Encrypt refresh token [refreshToken] using the bot's copy
@@ -240,41 +267,43 @@ export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { in
* @param {String} obj.refreshToken - refresh token * @param {String} obj.refreshToken - refresh token
*/ */
export const setIntegrationAuthRefreshHelper = async ({ export const setIntegrationAuthRefreshHelper = async ({
integrationAuthId, integrationAuthId,
refreshToken refreshToken,
}: { }: {
integrationAuthId: string; integrationAuthId: string;
refreshToken: string; refreshToken: string;
}) => { }) => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
let integrationAuth = await IntegrationAuth
.findById(integrationAuthId); if (!integrationAuth) throw new Error("Failed to find integration auth");
if (!integrationAuth) throw new Error('Failed to find integration auth'); const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
const obj = await BotService.encryptSymmetric({ plaintext: refreshToken,
workspaceId: integrationAuth.workspace, });
plaintext: refreshToken
}); integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
integrationAuth = await IntegrationAuth.findOneAndUpdate({ _id: integrationAuthId,
_id: integrationAuthId },
}, { {
refreshCiphertext: obj.ciphertext, refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv, refreshIV: obj.iv,
refreshTag: obj.tag, refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8,
}, { },
new: true {
}); new: true,
}
return integrationAuth; );
}
return integrationAuth;
};
/** /**
* Encrypt access token [accessToken] and (optionally) access id [accessId] * Encrypt access token [accessToken] and (optionally) access id [accessId]
* using the bot's copy of the workspace key for workspace belonging to * using the bot's copy of the workspace key for workspace belonging to
* integration auth with id [integrationAuthId] and store it along with [accessExpiresAt] * integration auth with id [integrationAuthId] and store it along with [accessExpiresAt]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
@@ -282,48 +311,52 @@ export const setIntegrationAuthRefreshHelper = async ({
* @param {Date} obj.accessExpiresAt - expiration date of access token * @param {Date} obj.accessExpiresAt - expiration date of access token
*/ */
export const setIntegrationAuthAccessHelper = async ({ export const setIntegrationAuthAccessHelper = async ({
integrationAuthId, integrationAuthId,
accessId, accessId,
accessToken, accessToken,
accessExpiresAt accessExpiresAt,
}: { }: {
integrationAuthId: string; integrationAuthId: string;
accessId: string | null; accessId: string | null;
accessToken: string; accessToken: string;
accessExpiresAt: Date | undefined; accessExpiresAt: Date | undefined;
}) => { }) => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId); let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth'); if (!integrationAuth) throw new Error("Failed to find integration auth");
const encryptedAccessTokenObj = await BotService.encryptSymmetric({ const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace, workspaceId: integrationAuth.workspace,
plaintext: accessToken plaintext: accessToken,
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessId,
}); });
}
let encryptedAccessIdObj;
if (accessId) { integrationAuth = await IntegrationAuth.findOneAndUpdate(
encryptedAccessIdObj = await BotService.encryptSymmetric({ {
workspaceId: integrationAuth.workspace, _id: integrationAuthId,
plaintext: accessId },
}); {
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
},
{
new: true,
} }
);
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId return integrationAuth;
}, { };
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
return integrationAuth;
}
+30 -24
View File
@@ -15,7 +15,7 @@ import {
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT INTEGRATION_HASHICORP_VAULT,
} from "../variables"; } from "../variables";
export interface IIntegration { export interface IIntegration {
@@ -33,23 +33,24 @@ export interface IIntegration {
targetServiceId: string; targetServiceId: string;
path: string; path: string;
region: string; region: string;
secretPath: string;
integration: integration:
| 'azure-key-vault' | "azure-key-vault"
| 'aws-parameter-store' | "aws-parameter-store"
| 'aws-secret-manager' | "aws-secret-manager"
| 'heroku' | "heroku"
| 'vercel' | "vercel"
| 'netlify' | "netlify"
| 'github' | "github"
| 'gitlab' | "gitlab"
| 'render' | "render"
| 'railway' | "railway"
| 'flyio' | "flyio"
| 'circleci' | "circleci"
| 'travisci' | "travisci"
| 'supabase' | "supabase"
| 'checkly' | "checkly"
| 'hashicorp-vault'; | "hashicorp-vault";
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
@@ -71,7 +72,7 @@ const integrationSchema = new Schema<IIntegration>(
url: { url: {
// for custom self-hosted integrations (e.g. self-hosted GitHub enterprise) // for custom self-hosted integrations (e.g. self-hosted GitHub enterprise)
type: String, type: String,
default: null default: null,
}, },
app: { app: {
// name of app in provider // name of app in provider
@@ -90,17 +91,17 @@ const integrationSchema = new Schema<IIntegration>(
}, },
targetEnvironmentId: { targetEnvironmentId: {
type: String, type: String,
default: null default: null,
}, },
targetService: { targetService: {
// railway-specific service // railway-specific service
type: String, type: String,
default: null default: null,
}, },
targetServiceId: { targetServiceId: {
// railway-specific service // railway-specific service
type: String, type: String,
default: null default: null,
}, },
owner: { owner: {
// github-specific repo owner-login // github-specific repo owner-login
@@ -111,12 +112,12 @@ const integrationSchema = new Schema<IIntegration>(
// aws-parameter-store-specific path // aws-parameter-store-specific path
// (also) vercel preview-branch // (also) vercel preview-branch
type: String, type: String,
default: null default: null,
}, },
region: { region: {
// aws-parameter-store-specific path // aws-parameter-store-specific path
type: String, type: String,
default: null default: null,
}, },
integration: { integration: {
type: String, type: String,
@@ -136,7 +137,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT INTEGRATION_HASHICORP_VAULT,
], ],
required: true, required: true,
}, },
@@ -145,6 +146,11 @@ const integrationSchema = new Schema<IIntegration>(
ref: "IntegrationAuth", ref: "IntegrationAuth",
required: true, required: true,
}, },
secretPath: {
type: String,
required: true,
default: "/",
},
}, },
{ {
timestamps: true, timestamps: true,
+63 -61
View File
@@ -1,75 +1,77 @@
import express from 'express'; import express from "express";
const router = express.Router(); const router = express.Router();
import { import {
requireAuth, requireAuth,
requireIntegrationAuth, requireIntegrationAuth,
requireIntegrationAuthorizationAuth, requireIntegrationAuthorizationAuth,
validateRequest validateRequest,
} from '../../middleware'; } from "../../middleware";
import { import {
ADMIN, ADMIN,
MEMBER, MEMBER,
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY AUTH_MODE_API_KEY,
} from '../../variables'; } 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( router.post(
'/', "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}), }),
requireIntegrationAuthorizationAuth({ requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
location: 'body' location: "body",
}), }),
body('integrationAuthId').exists().isString().trim(), body("integrationAuthId").exists().isString().trim(),
body('app').trim(), body("app").trim(),
body('isActive').exists().isBoolean(), body("isActive").exists().isBoolean(),
body('appId').trim(), body("appId").trim(),
body('sourceEnvironment').trim(), body("secretPath").default("/").isString().trim(),
body('targetEnvironment').trim(), body("sourceEnvironment").trim(),
body('targetEnvironmentId').trim(), body("targetEnvironment").trim(),
body('targetService').trim(), body("targetEnvironmentId").trim(),
body('targetServiceId').trim(), body("targetService").trim(),
body('owner').trim(), body("targetServiceId").trim(),
body('path').trim(), body("owner").trim(),
body('region').trim(), body("path").trim(),
validateRequest, body("region").trim(),
integrationController.createIntegration validateRequest,
integrationController.createIntegration
); );
router.patch( router.patch(
'/:integrationId', "/:integrationId",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT] acceptedAuthModes: [AUTH_MODE_JWT],
}), }),
requireIntegrationAuth({ requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
}), }),
param('integrationId').exists().trim(), param("integrationId").exists().trim(),
body('isActive').exists().isBoolean(), body("isActive").exists().isBoolean(),
body('app').exists().trim(), body("app").exists().trim(),
body('environment').exists().trim(), body("secretPath").default("/").isString().trim(),
body('appId').exists(), body("environment").exists().trim(),
body('targetEnvironment').exists(), body("appId").exists(),
body('owner').exists(), body("targetEnvironment").exists(),
validateRequest, body("owner").exists(),
integrationController.updateIntegration validateRequest,
integrationController.updateIntegration
); );
router.delete( router.delete(
'/:integrationId', "/:integrationId",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT] acceptedAuthModes: [AUTH_MODE_JWT],
}), }),
requireIntegrationAuth({ requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
}), }),
param('integrationId').exists().trim(), param("integrationId").exists().trim(),
validateRequest, validateRequest,
integrationController.deleteIntegration integrationController.deleteIntegration
); );
export default router; export default router;
+103 -101
View File
@@ -1,110 +1,112 @@
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import {
getSecretsBotHelper, getSecretsBotHelper,
encryptSymmetricHelper, encryptSymmetricHelper,
decryptSymmetricHelper, decryptSymmetricHelper,
getKey, getKey,
getIsWorkspaceE2EEHelper getIsWorkspaceE2EEHelper,
} from '../helpers/bot'; } from "../helpers/bot";
/** /**
* Class to handle bot actions * Class to handle bot actions
*/ */
class BotService { class BotService {
/**
/** * Return whether or not workspace with id [workspaceId] is end-to-end encrypted
* Return whether or not workspace with id [workspaceId] is end-to-end encrypted * @param workspaceId - id of workspace
* @param workspaceId - id of workspace * @returns {Boolean}
* @returns {Boolean} */
*/ static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) {
static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) { return await getIsWorkspaceE2EEHelper(workspaceId);
return await getIsWorkspaceE2EEHelper(workspaceId); }
}
/** /**
* Get workspace key for workspace with id [workspaceId] shared to bot. * Get workspace key for workspace with id [workspaceId] shared to bot.
* @param {Object} obj * @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for * @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for
* @returns * @returns
*/ */
static async getWorkspaceKeyWithBot({ static async getWorkspaceKeyWithBot({
workspaceId workspaceId,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) { }) {
return await getKey({ return await getKey({
workspaceId workspaceId,
}); });
} }
/** /**
* Return decrypted secrets for workspace with id [workspaceId] and * Return decrypted secrets for workspace with id [workspaceId] and
* environment [environmen] shared to bot. * environment [environmen] shared to bot.
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId - id of workspace of secrets * @param {String} obj.workspaceId - id of workspace of secrets
* @param {String} obj.environment - environment for secrets * @param {String} obj.environment - environment for secrets
* @returns {Object} secretObj - object where keys are secret keys and values are secret values * @returns {Object} secretObj - object where keys are secret keys and values are secret values
*/ */
static async getSecrets({ static async getSecrets({
workspaceId, workspaceId,
environment environment,
}: { secretPath,
workspaceId: Types.ObjectId; }: {
environment: string; workspaceId: Types.ObjectId;
}) { environment: string;
return await getSecretsBotHelper({ secretPath: string;
workspaceId, }) {
environment return await getSecretsBotHelper({
}); workspaceId,
} environment,
secretPath,
/** });
* Return symmetrically encrypted [plaintext] using the }
* bot's copy of the workspace key for workspace with id [workspaceId]
* @param {Object} obj /**
* @param {String} obj.workspaceId - id of workspace * Return symmetrically encrypted [plaintext] using the
* @param {String} obj.plaintext - plaintext to encrypt * bot's copy of the workspace key for workspace with id [workspaceId]
*/ * @param {Object} obj
static async encryptSymmetric({ * @param {String} obj.workspaceId - id of workspace
workspaceId, * @param {String} obj.plaintext - plaintext to encrypt
plaintext */
}: { static async encryptSymmetric({
workspaceId: Types.ObjectId; workspaceId,
plaintext: string; plaintext,
}) { }: {
return await encryptSymmetricHelper({ workspaceId: Types.ObjectId;
workspaceId, plaintext: string;
plaintext }) {
}); return await encryptSymmetricHelper({
} workspaceId,
plaintext,
/** });
* Return symmetrically decrypted [ciphertext] using the }
* bot's copy of the workspace key for workspace with id [workspaceId]
* @param {Object} obj /**
* @param {String} obj.workspaceId - id of workspace * Return symmetrically decrypted [ciphertext] using the
* @param {String} obj.ciphertext - ciphertext to decrypt * bot's copy of the workspace key for workspace with id [workspaceId]
* @param {String} obj.iv - iv * @param {Object} obj
* @param {String} obj.tag - tag * @param {String} obj.workspaceId - id of workspace
*/ * @param {String} obj.ciphertext - ciphertext to decrypt
static async decryptSymmetric({ * @param {String} obj.iv - iv
workspaceId, * @param {String} obj.tag - tag
ciphertext, */
iv, static async decryptSymmetric({
tag workspaceId,
}: { ciphertext,
workspaceId: Types.ObjectId; iv,
ciphertext: string; tag,
iv: string; }: {
tag: string; workspaceId: Types.ObjectId;
}) { ciphertext: string;
return await decryptSymmetricHelper({ iv: string;
workspaceId, tag: string;
ciphertext, }) {
iv, return await decryptSymmetricHelper({
tag workspaceId,
}); ciphertext,
} iv,
tag,
});
}
} }
export default BotService; export default BotService;
+17
View File
@@ -13,6 +13,7 @@ import {
BackupPrivateKey, BackupPrivateKey,
IntegrationAuth, IntegrationAuth,
ServiceTokenData, ServiceTokenData,
Integration,
} from "../../models"; } from "../../models";
import { generateKeyPair } from "../../utils/crypto"; import { generateKeyPair } from "../../utils/crypto";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
@@ -424,3 +425,19 @@ export const backfillServiceToken = async () => {
); );
console.log("Migration: Service token migration v1 complete"); console.log("Migration: Service token migration v1 complete");
}; };
export const backfillIntegration = async () => {
await Integration.updateMany(
{
secretPath: {
$exists: false,
},
},
{
$set: {
secretPath: "/",
},
}
);
console.log("Migration: Integration migration v1 complete");
};
+2
View File
@@ -13,6 +13,7 @@ import {
backfillEncryptionMetadata, backfillEncryptionMetadata,
backfillSecretFolders, backfillSecretFolders,
backfillServiceToken, backfillServiceToken,
backfillIntegration,
} from "./backfillData"; } from "./backfillData";
import { import {
reencryptBotPrivateKeys, reencryptBotPrivateKeys,
@@ -77,6 +78,7 @@ export const setup = async () => {
await backfillEncryptionMetadata(); await backfillEncryptionMetadata();
await backfillSecretFolders(); await backfillSecretFolders();
await backfillServiceToken(); await backfillServiceToken();
await backfillIntegration();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

@@ -17,6 +17,12 @@
"image": "Kubernetes", "image": "Kubernetes",
"docsLink": "https://infisical.com/docs/integrations/platforms/kubernetes" "docsLink": "https://infisical.com/docs/integrations/platforms/kubernetes"
}, },
{
"name": "Terraform",
"slug": "terraform",
"image": "Terraform",
"docsLink": "https://infisical.com/docs/integrations/frameworks/terraform"
},
{ {
"name": "React", "name": "React",
"slug": "react", "slug": "react",
@@ -4,7 +4,11 @@ import { useRouter } from 'next/router';
import { faArrowRight, faCheck, faXmark } from '@fortawesome/free-solid-svg-icons'; import { faArrowRight, faCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// TODO: This needs to be moved from public folder // TODO: This needs to be moved from public folder
import { contextNetlifyMapping, integrationSlugNameMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants'; import {
contextNetlifyMapping,
integrationSlugNameMapping,
reverseContextNetlifyMapping
} from 'public/data/frequentConstants';
import Button from '@app/components/basic/buttons/Button'; import Button from '@app/components/basic/buttons/Button';
import ListBox from '@app/components/basic/Listbox'; import ListBox from '@app/components/basic/Listbox';
@@ -27,6 +31,7 @@ interface Integration {
targetEnvironment: string; targetEnvironment: string;
workspace: string; workspace: string;
integrationAuth: string; integrationAuth: string;
secretPath: string;
} }
interface IntegrationApp { interface IntegrationApp {
@@ -55,7 +60,6 @@ const IntegrationTile = ({
environments = [], environments = [],
handleDeleteIntegration handleDeleteIntegration
}: Props) => { }: Props) => {
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>( const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
environments.find(({ slug }) => slug === integration?.environment) || { environments.find(({ slug }) => slug === integration?.environment) || {
name: '', name: '',
@@ -74,16 +78,16 @@ const IntegrationTile = ({
}); });
setApps(tempApps); setApps(tempApps);
if (integration?.app) { if (integration?.app) {
setIntegrationApp(integration.app); setIntegrationApp(integration.app);
} else if (integration?.path && integration?.region) { } else if (integration?.path && integration?.region) {
setIntegrationApp(`${integration.path} (${integration.region})`); setIntegrationApp(`${integration.path} (${integration.region})`);
} else if (tempApps.length > 0) { } else if (tempApps.length > 0) {
setIntegrationApp(tempApps[0].name) setIntegrationApp(tempApps[0].name);
} else { } else {
setIntegrationApp(''); setIntegrationApp('');
} }
switch (integration.integration) { switch (integration.integration) {
case 'vercel': case 'vercel':
@@ -212,12 +216,15 @@ const IntegrationTile = ({
return <div />; return <div />;
}; };
if (!integrationApp && integration.integration !== "checkly") return <div />; if (!integrationApp && integration.integration !== 'checkly') return <div />;
const isSelected = integration.integration === 'hashicorp-vault' ? `${integration.app} - path: ${integration.path}` : integrationApp; const isSelected =
integration.integration === 'hashicorp-vault'
? `${integration.app} - path: ${integration.path}`
: integrationApp;
return ( return (
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-mineshaft-800 border border-mineshaft-600 p-6"> <div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6">
<div className="flex"> <div className="flex">
<div> <div>
<p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p> <p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p>
@@ -235,33 +242,46 @@ const IntegrationTile = ({
isFull isFull
/> />
</div> </div>
<div className="ml-2">
<p className="mb-2 text-xs font-semibold text-gray-400">SECRET PATH</p>
<div className="cursor-default rounded-md bg-white/[.07] py-2.5 pl-4 pr-10 text-sm font-semibold text-gray-300">
{/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */}
{integration.secretPath}
</div>
</div>
<div className="pt-2"> <div className="pt-2">
<FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" /> <FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" />
</div> </div>
<div className="mr-2"> <div className="mr-2">
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p> <p className="mb-2 text-xs font-semibold text-gray-400">INTEGRATION</p>
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300 cursor-default"> <div className="cursor-default rounded-md bg-white/[.07] py-2.5 pl-4 pr-10 text-sm font-semibold text-gray-300">
{/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */} {/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */}
{integrationSlugNameMapping[integration.integration]} {integrationSlugNameMapping[integration.integration]}
</div> </div>
</div> </div>
<div className="mr-2"> <div className="mr-2">
<div className="mb-2 text-xs font-semibold text-gray-400">APP</div> <div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
{integrationApp ? <div title={integrationApp}> {integrationApp ? (
<ListBox <div title={integrationApp}>
data={!integration.isActive ? apps.map((app) => app.name) : null} <ListBox
isSelected={isSelected} data={!integration.isActive ? apps.map((app) => app.name) : null}
onChange={(app) => { isSelected={isSelected}
setIntegrationApp(app); onChange={(app) => {
}} setIntegrationApp(app);
/> }}
</div> : <div className='w-52 h-10 rounded-md bg-mineshaft-600 animate-pulse px-4 font-bold py-2'>-</div>} />
</div>
) : (
<div className="h-10 w-52 animate-pulse rounded-md bg-mineshaft-600 px-4 py-2 font-bold">
-
</div>
)}
</div> </div>
{renderIntegrationSpecificParams(integration)} {renderIntegrationSpecificParams(integration)}
</div> </div>
<div className="flex items-end cursor-default"> <div className="flex cursor-default items-end">
{integration.isActive ? ( {integration.isActive ? (
<div className="flex max-w-5xl flex-row items-center rounded-md bg-mineshaft-600 p-[0.44rem] px-4 border border-mineshaft-500"> <div className="flex max-w-5xl flex-row items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 p-[0.44rem] px-4">
<FontAwesomeIcon icon={faCheck} className="mr-2.5 text-lg text-primary" /> <FontAwesomeIcon icon={faCheck} className="mr-2.5 text-lg text-primary" />
<div className="font-semibold text-gray-300">In Sync</div> <div className="font-semibold text-gray-300">In Sync</div>
</div> </div>
@@ -23,6 +23,7 @@ interface Integration {
targetEnvironment: string; targetEnvironment: string;
workspace: string; workspace: string;
integrationAuth: string; integrationAuth: string;
secretPath: string;
} }
const ProjectIntegrationSection = ({ const ProjectIntegrationSection = ({
@@ -3,6 +3,7 @@ import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props { interface Props {
integrationAuthId: string; integrationAuthId: string;
isActive: boolean; isActive: boolean;
secretPath: string;
app: string | null; app: string | null;
appId: string | null; appId: string | null;
sourceEnvironment: string; sourceEnvironment: string;
@@ -20,19 +21,20 @@ interface Props {
* @param {String} obj.accessToken - id of integration authorization for which to create the integration * @param {String} obj.accessToken - id of integration authorization for which to create the integration
* @returns * @returns
*/ */
const createIntegration = ({ const createIntegration = ({
integrationAuthId, integrationAuthId,
isActive, isActive,
app, app,
appId, appId,
sourceEnvironment, sourceEnvironment,
targetEnvironment, targetEnvironment,
targetEnvironmentId, targetEnvironmentId,
targetService, targetService,
targetServiceId, targetServiceId,
owner, owner,
path, path,
region region,
secretPath
}: Props) => }: Props) =>
SecurityClient.fetchCall('/api/v1/integration', { SecurityClient.fetchCall('/api/v1/integration', {
method: 'POST', method: 'POST',
@@ -40,18 +42,19 @@ const createIntegration = ({
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
integrationAuthId, integrationAuthId,
isActive, isActive,
app, app,
appId, appId,
sourceEnvironment, sourceEnvironment,
targetEnvironment, targetEnvironment,
targetEnvironmentId, targetEnvironmentId,
targetService, targetService,
targetServiceId, targetServiceId,
owner, owner,
path, path,
region region,
secretPath
}) })
}).then(async (res) => { }).then(async (res) => {
if (res && res.status === 200) { if (res && res.status === 200) {
@@ -61,4 +64,4 @@ const createIntegration = ({
return undefined; return undefined;
}); });
export default createIntegration; export default createIntegration;
+10 -1
View File
@@ -44,6 +44,7 @@ interface Integration {
integration: string; integration: string;
targetEnvironment: string; targetEnvironment: string;
workspace: string; workspace: string;
secretPath:string;
integrationAuth: string; integrationAuth: string;
} }
@@ -436,7 +437,15 @@ export default function Integrations() {
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth} handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
/> />
) : ( ) : (
<div /> <>
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">{t('integrations.cloud-integrations')}</h1>
<p className="text-base text-gray-400">{t('integrations.click-to-start')}</p>
</div>
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map(elem => <div key={elem} className="bg-mineshaft-800 border border-mineshaft-600 animate-pulse h-32 rounded-md"/>)}
</div>
</>
)} )}
<FrameworkIntegrationSection frameworks={frameworkIntegrationOptions as any} /> <FrameworkIntegrationSection frameworks={frameworkIntegrationOptions as any} />
</div> </div>
@@ -56,6 +56,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [selectedAWSRegion, setSelectedAWSRegion] = useState(''); const [selectedAWSRegion, setSelectedAWSRegion] = useState('');
const [path, setPath] = useState(''); const [path, setPath] = useState('');
const [pathErrorText, setPathErrorText] = useState(''); const [pathErrorText, setPathErrorText] = useState('');
@@ -69,9 +70,9 @@ export default function AWSParameterStoreCreateIntegrationPage() {
} }
}, [workspace]); }, [workspace]);
const isValidAWSParameterStorePath = (secretPath: string) => { const isValidAWSParameterStorePath = (awsStorePath: string) => {
const pattern = /^\/([\w-]+\/)*[\w-]+\/$/; const pattern = /^\/([\w-]+\/)*[\w-]+\/$/;
return pattern.test(secretPath) && secretPath.length <= 2048; return pattern.test(awsStorePath) && awsStorePath.length <= 2048;
}; };
const handleButtonClick = async () => { const handleButtonClick = async () => {
@@ -101,7 +102,8 @@ export default function AWSParameterStoreCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path, path,
region: selectedAWSRegion region: selectedAWSRegion,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -133,6 +135,13 @@ export default function AWSParameterStoreCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="AWS Region"> <FormControl label="AWS Region">
<Select <Select
value={selectedAWSRegion} value={selectedAWSRegion}
@@ -56,6 +56,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [selectedAWSRegion, setSelectedAWSRegion] = useState(''); const [selectedAWSRegion, setSelectedAWSRegion] = useState('');
const [targetSecretName, setTargetSecretName] = useState(''); const [targetSecretName, setTargetSecretName] = useState('');
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState(''); const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState('');
@@ -100,7 +101,8 @@ export default function AWSSecretManagerCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: selectedAWSRegion region: selectedAWSRegion,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -132,6 +134,13 @@ export default function AWSSecretManagerCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="AWS Region"> <FormControl label="AWS Region">
<Select <Select
value={selectedAWSRegion} value={selectedAWSRegion}
@@ -24,6 +24,7 @@ export default function AzureKeyVaultCreateIntegrationPage() {
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [vaultBaseUrl, setVaultBaseUrl] = useState(''); const [vaultBaseUrl, setVaultBaseUrl] = useState('');
const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState(''); const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState('');
@@ -63,7 +64,8 @@ export default function AzureKeyVaultCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -93,6 +95,13 @@ export default function AzureKeyVaultCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl <FormControl
label="Vault URI" label="Vault URI"
errorText={vaultBaseUrlErrorText} errorText={vaultBaseUrlErrorText}
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -22,6 +30,8 @@ export default function ChecklyCreateIntegrationPage() {
}); });
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [targetAppId, setTargetAppId] = useState(''); const [targetAppId, setTargetAppId] = useState('');
@@ -63,7 +73,8 @@ export default function ChecklyCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -80,8 +91,13 @@ export default function ChecklyCreateIntegrationPage() {
integrationAuthApps && integrationAuthApps &&
targetApp ? ( targetApp ? (
<div className="flex h-full w-full items-center justify-center bg-gradient-to-tr from-mineshaft-900 to-bunker-900"> <div className="flex h-full w-full items-center justify-center bg-gradient-to-tr from-mineshaft-900 to-bunker-900">
<Card className="max-w-lg rounded-md p-0 border border-mineshaft-600"> <Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
<CardTitle className="text-left px-6" subTitle="Choose which environment in Infisical you want to sync with your Checkly account.">Checkly Integration</CardTitle> <CardTitle
className="px-6 text-left"
subTitle="Choose which environment in Infisical you want to sync with your Checkly account."
>
Checkly Integration
</CardTitle>
<FormControl label="Infisical Project Environment" className="mt-2 px-6"> <FormControl label="Infisical Project Environment" className="mt-2 px-6">
<Select <Select
value={selectedSourceEnvironment} value={selectedSourceEnvironment}
@@ -98,6 +114,13 @@ export default function ChecklyCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Checkly Account" className="mt-4 px-6"> <FormControl label="Checkly Account" className="mt-4 px-6">
<Select <Select
value={targetApp} value={targetApp}
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -22,6 +30,8 @@ export default function CircleCICreateIntegrationPage() {
}); });
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -62,7 +72,8 @@ export default function CircleCICreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -97,6 +108,13 @@ export default function CircleCICreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="CircleCI Project" className="mt-4"> <FormControl label="CircleCI Project" className="mt-4">
<Select <Select
value={targetApp} value={targetApp}
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -22,6 +30,8 @@ export default function FlyioCreateIntegrationPage() {
}); });
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -61,7 +71,8 @@ export default function FlyioCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -96,6 +107,13 @@ export default function FlyioCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Fly.io App" className="mt-4"> <FormControl label="Fly.io App" className="mt-4">
<Select <Select
value={targetApp} value={targetApp}
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -22,6 +30,7 @@ export default function GitHubCreateIntegrationPage() {
}); });
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetAppId, setTargetAppId] = useState(''); const [targetAppId, setTargetAppId] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -66,7 +75,8 @@ export default function GitHubCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: targetApp.owner, owner: targetApp.owner,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -100,6 +110,13 @@ export default function GitHubCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="GitHub Repo" className="mt-4"> <FormControl label="GitHub Repo" className="mt-4">
<Select <Select
value={targetAppId} value={targetAppId}
@@ -2,13 +2,13 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { import {
Button, Button,
Card, Card,
CardTitle, CardTitle,
FormControl, FormControl,
Input, Input,
Select, Select,
SelectItem SelectItem
} from '../../../components/v2'; } from '../../../components/v2';
import { import {
@@ -44,6 +44,7 @@ export default function GitLabCreateIntegrationPage() {
const [targetEntity, setTargetEntity] = useState(gitLabEntities[0].value); const [targetEntity, setTargetEntity] = useState(gitLabEntities[0].value);
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetAppId, setTargetAppId] = useState(''); const [targetAppId, setTargetAppId] = useState('');
const [targetEnvironment, setTargetEnvironment] = useState(''); const [targetEnvironment, setTargetEnvironment] = useState('');
@@ -101,7 +102,8 @@ export default function GitLabCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -136,6 +138,13 @@ export default function GitLabCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="GitLab Integration Type"> <FormControl label="GitLab Integration Type">
<Select <Select
value={targetEntity} value={targetEntity}
@@ -198,13 +207,11 @@ export default function GitLabCreateIntegrationPage() {
)} )}
</Select> </Select>
</FormControl> </FormControl>
<FormControl <FormControl label="GitLab Environment Scope (Optional)">
label="GitLab Environment Scope (Optional)" <Input
> placeholder="*"
<Input value={targetEnvironment}
placeholder="*" onChange={(e) => setTargetEnvironment(e.target.value)}
value={targetEnvironment}
onChange={(e) => setTargetEnvironment(e.target.value)}
/> />
</FormControl> </FormControl>
<Button <Button
@@ -1,8 +1,16 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Input, Select, SelectItem } from '../../../components/v2';
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth'; import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace'; import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from '../../api/integrations/createIntegration'; import createIntegration from '../../api/integrations/createIntegration';
@@ -16,60 +24,58 @@ export default function HashiCorpVaultCreateIntegrationPage() {
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ''); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
const [vaultEnginePath, setVaultEnginePath] = useState(''); const [vaultEnginePath, setVaultEnginePath] = useState('');
const [vaultEnginePathErrorText, setVaultEnginePathErrorText ] = useState(''); const [vaultEnginePathErrorText, setVaultEnginePathErrorText] = useState('');
const [vaultSecretPath, setVaultSecretPath] = useState(''); const [vaultSecretPath, setVaultSecretPath] = useState('');
const [vaultSecretPathErrorText, setVaultSecretPathErrorText] = useState(''); const [vaultSecretPathErrorText, setVaultSecretPathErrorText] = useState('');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const isValidVaultPath = (secretPath: string) => { const isValidVaultPath = (vaultPath: string) => {
return !( return !(vaultPath.length === 0 || vaultPath.startsWith('/') || vaultPath.endsWith('/'));
secretPath.length === 0 ||
secretPath.startsWith('/') ||
secretPath.endsWith('/')
);
}; };
const handleButtonClick = async () => { const handleButtonClick = async () => {
try { try {
if (!integrationAuth?._id) return; if (!integrationAuth?._id) return;
if (!isValidVaultPath(vaultEnginePath)) { if (!isValidVaultPath(vaultEnginePath)) {
setVaultEnginePathErrorText('Vault KV Secrets Engine Path must be valid like kv'); setVaultEnginePathErrorText('Vault KV Secrets Engine Path must be valid like kv');
} else { } else {
setVaultEnginePathErrorText(''); setVaultEnginePathErrorText('');
} }
if (!isValidVaultPath(vaultSecretPath)) {
setVaultSecretPathErrorText('Vault Secret(s) Path must be valid like machine/dev');
} else {
setVaultSecretPathErrorText('');
}
if (!isValidVaultPath || !isValidVaultPath(vaultSecretPath)) return;
setIsLoading(true); if (!isValidVaultPath(vaultSecretPath)) {
setVaultSecretPathErrorText('Vault Secret(s) Path must be valid like machine/dev');
await createIntegration({ } else {
integrationAuthId: integrationAuth?._id, setVaultSecretPathErrorText('');
isActive: true, }
app: vaultEnginePath,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: vaultSecretPath,
region: null
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem('projectData.id')}`); if (!isValidVaultPath || !isValidVaultPath(vaultSecretPath)) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: vaultEnginePath,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: vaultSecretPath,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem('projectData.id')}`);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@@ -95,34 +101,41 @@ export default function HashiCorpVaultCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl <FormControl
label="Vault KV Secrets Engine Path" label="Vault KV Secrets Engine Path"
errorText={vaultEnginePathErrorText} errorText={vaultEnginePathErrorText}
isError={vaultEnginePathErrorText !== '' ?? false} isError={vaultEnginePathErrorText !== '' ?? false}
> >
<Input <Input
placeholder="kv" placeholder="kv"
value={vaultEnginePath} value={vaultEnginePath}
onChange={(e) => setVaultEnginePath(e.target.value)} onChange={(e) => setVaultEnginePath(e.target.value)}
/> />
</FormControl> </FormControl>
<FormControl <FormControl
label="Vault Secret(s) Path" label="Vault Secret(s) Path"
errorText={vaultSecretPathErrorText} errorText={vaultSecretPathErrorText}
isError={vaultSecretPathErrorText !== '' ?? false} isError={vaultSecretPathErrorText !== '' ?? false}
> >
<Input <Input
placeholder="machine/dev" placeholder="machine/dev"
value={vaultSecretPath} value={vaultSecretPath}
onChange={(e) => setVaultSecretPath(e.target.value)} onChange={(e) => setVaultSecretPath(e.target.value)}
/> />
</FormControl> </FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"
className="mt-4" className="mt-4"
isLoading={isLoading} isLoading={isLoading}
isDisabled={!(isValidVaultPath(vaultEnginePath) && isValidVaultPath(vaultSecretPath))} isDisabled={!(isValidVaultPath(vaultEnginePath) && isValidVaultPath(vaultSecretPath))}
> >
Create Integration Create Integration
</Button> </Button>
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -23,6 +31,7 @@ export default function HerokuCreateIntegrationPage() {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -60,7 +69,8 @@ export default function HerokuCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -94,6 +104,13 @@ export default function HerokuCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Heroku App" className="mt-4"> <FormControl label="Heroku App" className="mt-4">
<Select <Select
value={targetApp} value={targetApp}
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -31,7 +39,7 @@ export default function NetlifyCreateIntegrationPage() {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [targetEnvironment, setTargetEnvironment] = useState(''); const [targetEnvironment, setTargetEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
@@ -71,7 +79,8 @@ export default function NetlifyCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -106,6 +115,13 @@ export default function NetlifyCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Netlify Site"> <FormControl label="Netlify Site">
<Select <Select
value={targetApp} value={targetApp}
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById, useGetIntegrationAuthById,
@@ -20,6 +28,7 @@ export default function RailwayCreateIntegrationPage() {
const [targetServiceId, setTargetServiceId] = useState(''); const [targetServiceId, setTargetServiceId] = useState('');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]); const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
@@ -99,7 +108,8 @@ export default function RailwayCreateIntegrationPage() {
targetServiceId: targetService ? targetService.serviceId : null, targetServiceId: targetService ? targetService.serviceId : null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -134,6 +144,13 @@ export default function RailwayCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Railway Project"> <FormControl label="Railway Project">
<Select <Select
value={targetAppId} value={targetAppId}
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -23,7 +31,7 @@ export default function RenderCreateIntegrationPage() {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
@@ -62,7 +70,8 @@ export default function RenderCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -97,6 +106,13 @@ export default function RenderCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Render Service" className="mt-4"> <FormControl label="Render Service" className="mt-4">
<Select <Select
value={targetApp} value={targetApp}
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -22,6 +30,7 @@ export default function SupabaseCreateIntegrationPage() {
}); });
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -62,7 +71,8 @@ export default function SupabaseCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -97,6 +107,13 @@ export default function SupabaseCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Supabase Project" className="mt-4"> <FormControl label="Supabase Project" className="mt-4">
<Select <Select
value={targetApp} value={targetApp}
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -23,7 +31,7 @@ export default function TravisCICreateIntegrationPage() {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState(''); const [targetApp, setTargetApp] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
@@ -62,7 +70,8 @@ export default function TravisCICreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path: null, path: null,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -97,6 +106,13 @@ export default function TravisCICreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Travis CI Project" className="mt-4"> <FormControl label="Travis CI Project" className="mt-4">
<Select <Select
value={targetApp} value={targetApp}
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import queryString from 'query-string'; import queryString from 'query-string';
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2'; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById, useGetIntegrationAuthById,
@@ -21,6 +29,7 @@ export default function VercelCreateIntegrationPage() {
const router = useRouter(); const router = useRouter();
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [secretPath, setSecretPath] = useState('/');
const [targetAppId, setTargetAppId] = useState(''); const [targetAppId, setTargetAppId] = useState('');
const [targetEnvironment, setTargetEnvironment] = useState(''); const [targetEnvironment, setTargetEnvironment] = useState('');
const [targetBranch, setTargetBranch] = useState(''); const [targetBranch, setTargetBranch] = useState('');
@@ -84,7 +93,8 @@ export default function VercelCreateIntegrationPage() {
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
path, path,
region: null region: null,
secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -119,6 +129,13 @@ export default function VercelCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Vercel App"> <FormControl label="Vercel App">
<Select <Select
value={targetAppId} value={targetAppId}