mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #650 from akhilmhdh/feat/integrations-page
Feat/integrations page
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { Types } from 'mongoose';
|
||||
import {
|
||||
Integration
|
||||
} from '../../models';
|
||||
import { EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Integration } from "../../models";
|
||||
import { EventService } from "../../services";
|
||||
import { eventPushSecrets } from "../../events";
|
||||
import Folder from "../../models/folder";
|
||||
import { getFolderByPath } from "../../services/FolderService";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
|
||||
/**
|
||||
* Create/initialize an (empty) integration for integration authorization
|
||||
@@ -25,9 +26,24 @@ export const createIntegration = async (req: Request, res: Response) => {
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region
|
||||
region,
|
||||
secretPath,
|
||||
} = 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]
|
||||
|
||||
// initialize new integration after saving integration access token
|
||||
@@ -44,17 +60,18 @@ export const createIntegration = async (req: Request, res: Response) => {
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
secretPath,
|
||||
integration: req.integrationAuth.integration,
|
||||
integrationAuth: new Types.ObjectId(integrationAuthId)
|
||||
integrationAuth: new Types.ObjectId(integrationAuthId),
|
||||
}).save();
|
||||
|
||||
|
||||
if (integration) {
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: integration.workspace,
|
||||
environment: sourceEnvironment
|
||||
})
|
||||
environment: sourceEnvironment,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,7 +87,6 @@ export const createIntegration = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const updateIntegration = async (req: Request, res: Response) => {
|
||||
|
||||
// TODO: add integration-specific validation to ensure that each
|
||||
// integration has the correct fields populated in [Integration]
|
||||
|
||||
@@ -81,8 +97,23 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner, // github-specific integration param
|
||||
secretPath,
|
||||
} = 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(
|
||||
{
|
||||
_id: req.integration._id,
|
||||
@@ -94,6 +125,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner,
|
||||
secretPath,
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
@@ -105,7 +137,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: integration.workspace,
|
||||
environment
|
||||
environment,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
Bot,
|
||||
BotKey,
|
||||
Secret,
|
||||
ISecret,
|
||||
IUser
|
||||
} from "../models";
|
||||
import { Bot, BotKey, Secret, ISecret, IUser } from "../models";
|
||||
import {
|
||||
generateKeyPair,
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
decryptSymmetric128BitHexKeyUTF8,
|
||||
decryptAsymmetric
|
||||
} from '../utils/crypto';
|
||||
decryptAsymmetric,
|
||||
} from "../utils/crypto";
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
ENCODING_SCHEME_BASE64
|
||||
ENCODING_SCHEME_BASE64,
|
||||
} from "../variables";
|
||||
import {
|
||||
getEncryptionKey,
|
||||
getRootEncryptionKey,
|
||||
client
|
||||
} from "../config";
|
||||
import { getEncryptionKey, getRootEncryptionKey, client } from "../config";
|
||||
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]
|
||||
@@ -40,15 +32,14 @@ export const createBot = async ({
|
||||
}) => {
|
||||
const encryptionKey = await getEncryptionKey();
|
||||
const rootEncryptionKey = await getRootEncryptionKey();
|
||||
|
||||
|
||||
const { publicKey, privateKey } = generateKeyPair();
|
||||
|
||||
|
||||
if (rootEncryptionKey) {
|
||||
const {
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
||||
const { ciphertext, iv, tag } = client.encryptSymmetric(
|
||||
privateKey,
|
||||
rootEncryptionKey
|
||||
);
|
||||
|
||||
return await new Bot({
|
||||
name,
|
||||
@@ -59,9 +50,8 @@ export const createBot = async ({
|
||||
iv,
|
||||
tag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_BASE64
|
||||
keyEncoding: ENCODING_SCHEME_BASE64,
|
||||
}).save();
|
||||
|
||||
} else if (encryptionKey) {
|
||||
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: privateKey,
|
||||
@@ -77,12 +67,12 @@ export const createBot = async ({
|
||||
iv,
|
||||
tag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
}).save();
|
||||
}
|
||||
|
||||
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) => {
|
||||
const botKey = await BotKey.exists({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
workspace: workspaceId,
|
||||
});
|
||||
|
||||
return botKey ? false : true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return decrypted secrets for workspace with id [workspaceId]
|
||||
@@ -108,16 +98,38 @@ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
|
||||
export const getSecretsBotHelper = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}) => {
|
||||
const content = {} as any;
|
||||
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({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
type: SECRET_SHARED,
|
||||
folder: folderId,
|
||||
});
|
||||
|
||||
secrets.forEach((secret: ISecret) => {
|
||||
@@ -148,14 +160,17 @@ export const getSecretsBotHelper = async ({
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @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 rootEncryptionKey = await getRootEncryptionKey();
|
||||
|
||||
const botKey = await BotKey.findOne({
|
||||
workspace: workspaceId,
|
||||
})
|
||||
.populate<{ sender: IUser }>("sender", "publicKey");
|
||||
}).populate<{ sender: IUser }>("sender", "publicKey");
|
||||
|
||||
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) {
|
||||
// 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({
|
||||
ciphertext: botKey.encryptedKey,
|
||||
@@ -177,15 +197,14 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
|
||||
privateKey: privateKeyBot,
|
||||
});
|
||||
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
|
||||
|
||||
// case: encoding scheme is utf8
|
||||
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: bot.encryptedPrivateKey,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
key: encryptionKey
|
||||
key: encryptionKey,
|
||||
});
|
||||
|
||||
|
||||
return decryptAsymmetric({
|
||||
ciphertext: botKey.encryptedKey,
|
||||
nonce: botKey.nonce,
|
||||
@@ -195,7 +214,8 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
Bot,
|
||||
Integration,
|
||||
IntegrationAuth
|
||||
} from '../models';
|
||||
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
||||
import { BotService } from '../services';
|
||||
import {
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_UTF8
|
||||
} from '../variables';
|
||||
import {
|
||||
UnauthorizedRequestError,
|
||||
} from '../utils/errors';
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
} from "../variables";
|
||||
import { UnauthorizedRequestError } from "../utils/errors";
|
||||
|
||||
interface Update {
|
||||
workspace: string;
|
||||
integration: string;
|
||||
teamId?: string;
|
||||
accountId?: string;
|
||||
workspace: string;
|
||||
integration: string;
|
||||
teamId?: string;
|
||||
accountId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,78 +25,83 @@ interface Update {
|
||||
* - Create bot sequence for integration
|
||||
* @param {Object} obj
|
||||
* @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
|
||||
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
|
||||
*/
|
||||
*/
|
||||
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,
|
||||
code,
|
||||
environment
|
||||
}: {
|
||||
workspaceId: string;
|
||||
integration: string;
|
||||
code: string;
|
||||
environment: string;
|
||||
}) => {
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
const update: Update = {
|
||||
workspace: workspaceId,
|
||||
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 (!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,
|
||||
code
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
const update: Update = {
|
||||
workspace: workspaceId,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return integrationAuth;
|
||||
};
|
||||
/**
|
||||
* Sync/push environment variables in workspace with id [workspaceId] to
|
||||
* all active integrations for that workspace
|
||||
@@ -110,48 +109,54 @@ export const handleOAuthExchangeHelper = async ({
|
||||
* @param {Object} obj.workspaceId - id of workspace
|
||||
*/
|
||||
export const syncIntegrationsHelper = async ({
|
||||
workspaceId,
|
||||
environment
|
||||
workspaceId,
|
||||
environment,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
}) => {
|
||||
const integrations = await Integration.find({
|
||||
workspace: workspaceId,
|
||||
...(environment ? {
|
||||
environment
|
||||
} : {}),
|
||||
isActive: true,
|
||||
app: { $ne: null }
|
||||
const integrations = await Integration.find({
|
||||
workspace: workspaceId,
|
||||
...(environment
|
||||
? {
|
||||
environment,
|
||||
}
|
||||
: {}),
|
||||
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
|
||||
// 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
|
||||
});
|
||||
const integrationAuth = await IntegrationAuth.findById(
|
||||
integration.integrationAuth
|
||||
);
|
||||
if (!integrationAuth) throw new Error("Failed to find integration auth");
|
||||
|
||||
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
// get integration auth access token
|
||||
const access = await getIntegrationAuthAccessHelper({
|
||||
integrationAuthId: integration.integrationAuth
|
||||
});
|
||||
// get integration auth access token
|
||||
const access = await getIntegrationAuthAccessHelper({
|
||||
integrationAuthId: integration.integrationAuth,
|
||||
});
|
||||
|
||||
// sync secrets to integration
|
||||
await syncSecrets({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessId: access.accessId === undefined ? null : access.accessId,
|
||||
accessToken: access.accessToken
|
||||
});
|
||||
}
|
||||
}
|
||||
// sync secrets to integration
|
||||
await syncSecrets({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessId: access.accessId === undefined ? null : access.accessId,
|
||||
accessToken: access.accessToken,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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} refreshToken - decrypted refresh token
|
||||
*/
|
||||
export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
|
||||
const integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId)
|
||||
.select('+refreshCiphertext +refreshIV +refreshTag');
|
||||
export const getIntegrationAuthRefreshHelper = async ({
|
||||
integrationAuthId,
|
||||
}: {
|
||||
integrationAuthId: Types.ObjectId;
|
||||
}) => {
|
||||
const integrationAuth = await IntegrationAuth.findById(
|
||||
integrationAuthId
|
||||
).select("+refreshCiphertext +refreshIV +refreshTag");
|
||||
|
||||
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
||||
|
||||
const refreshToken = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
ciphertext: integrationAuth.refreshCiphertext as string,
|
||||
iv: integrationAuth.refreshIV as string,
|
||||
tag: integrationAuth.refreshTag as string
|
||||
if (!integrationAuth)
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed to locate Integration Authentication credentials",
|
||||
});
|
||||
|
||||
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
|
||||
@@ -186,50 +198,65 @@ export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { i
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @returns {String} accessToken - decrypted access token
|
||||
*/
|
||||
export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
|
||||
let accessId;
|
||||
let accessToken;
|
||||
const integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId)
|
||||
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
|
||||
export const getIntegrationAuthAccessHelper = async ({
|
||||
integrationAuthId,
|
||||
}: {
|
||||
integrationAuthId: Types.ObjectId;
|
||||
}) => {
|
||||
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'});
|
||||
|
||||
accessToken = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
ciphertext: integrationAuth.accessCiphertext as string,
|
||||
iv: integrationAuth.accessIV as string,
|
||||
tag: integrationAuth.accessTag as string
|
||||
if (!integrationAuth)
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed to locate Integration Authentication credentials",
|
||||
});
|
||||
|
||||
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
||||
// there is a access token expiration date
|
||||
// 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
|
||||
});
|
||||
}
|
||||
accessToken = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
ciphertext: integrationAuth.accessCiphertext as string,
|
||||
iv: integrationAuth.accessIV as string,
|
||||
tag: integrationAuth.accessTag as string,
|
||||
});
|
||||
|
||||
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
||||
// there is a access token expiration date
|
||||
// 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({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
ciphertext: integrationAuth.accessIdCiphertext as string,
|
||||
iv: integrationAuth.accessIdIV as string,
|
||||
tag: integrationAuth.accessIdTag as string
|
||||
});
|
||||
}
|
||||
|
||||
return ({
|
||||
accessId,
|
||||
accessToken
|
||||
}
|
||||
|
||||
if (
|
||||
integrationAuth?.accessIdCiphertext &&
|
||||
integrationAuth?.accessIdIV &&
|
||||
integrationAuth?.accessIdTag
|
||||
) {
|
||||
accessId = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
ciphertext: integrationAuth.accessIdCiphertext as string,
|
||||
iv: integrationAuth.accessIdIV as string,
|
||||
tag: integrationAuth.accessIdTag as string,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
accessId,
|
||||
accessToken,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export const setIntegrationAuthRefreshHelper = async ({
|
||||
integrationAuthId,
|
||||
refreshToken
|
||||
integrationAuthId,
|
||||
refreshToken,
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
refreshToken: string;
|
||||
integrationAuthId: string;
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
|
||||
let integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
const obj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
plaintext: refreshToken
|
||||
});
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
_id: integrationAuthId
|
||||
}, {
|
||||
refreshCiphertext: obj.ciphertext,
|
||||
refreshIV: obj.iv,
|
||||
refreshTag: obj.tag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
|
||||
return integrationAuth;
|
||||
}
|
||||
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) throw new Error("Failed to find integration auth");
|
||||
|
||||
const obj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
plaintext: refreshToken,
|
||||
});
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
||||
{
|
||||
_id: integrationAuthId,
|
||||
},
|
||||
{
|
||||
refreshCiphertext: obj.ciphertext,
|
||||
refreshIV: obj.iv,
|
||||
refreshTag: obj.tag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
}
|
||||
);
|
||||
|
||||
return integrationAuth;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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]
|
||||
* @param {Object} obj
|
||||
* @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
|
||||
*/
|
||||
export const setIntegrationAuthAccessHelper = async ({
|
||||
integrationAuthId,
|
||||
accessId,
|
||||
accessToken,
|
||||
accessExpiresAt
|
||||
integrationAuthId,
|
||||
accessId,
|
||||
accessToken,
|
||||
accessExpiresAt,
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
accessExpiresAt: Date | undefined;
|
||||
integrationAuthId: string;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
accessExpiresAt: Date | undefined;
|
||||
}) => {
|
||||
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
plaintext: accessToken
|
||||
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) throw new Error("Failed to find integration auth");
|
||||
|
||||
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
plaintext: accessToken,
|
||||
});
|
||||
|
||||
let encryptedAccessIdObj;
|
||||
if (accessId) {
|
||||
encryptedAccessIdObj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
plaintext: accessId,
|
||||
});
|
||||
|
||||
let encryptedAccessIdObj;
|
||||
if (accessId) {
|
||||
encryptedAccessIdObj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
plaintext: accessId
|
||||
});
|
||||
}
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
||||
{
|
||||
_id: integrationAuthId,
|
||||
},
|
||||
{
|
||||
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
|
||||
}, {
|
||||
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;
|
||||
}
|
||||
);
|
||||
|
||||
return integrationAuth;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
INTEGRATION_TRAVISCI,
|
||||
INTEGRATION_SUPABASE,
|
||||
INTEGRATION_CHECKLY,
|
||||
INTEGRATION_HASHICORP_VAULT
|
||||
INTEGRATION_HASHICORP_VAULT,
|
||||
} from "../variables";
|
||||
|
||||
export interface IIntegration {
|
||||
@@ -33,23 +33,24 @@ export interface IIntegration {
|
||||
targetServiceId: string;
|
||||
path: string;
|
||||
region: string;
|
||||
secretPath: string;
|
||||
integration:
|
||||
| 'azure-key-vault'
|
||||
| 'aws-parameter-store'
|
||||
| 'aws-secret-manager'
|
||||
| 'heroku'
|
||||
| 'vercel'
|
||||
| 'netlify'
|
||||
| 'github'
|
||||
| 'gitlab'
|
||||
| 'render'
|
||||
| 'railway'
|
||||
| 'flyio'
|
||||
| 'circleci'
|
||||
| 'travisci'
|
||||
| 'supabase'
|
||||
| 'checkly'
|
||||
| 'hashicorp-vault';
|
||||
| "azure-key-vault"
|
||||
| "aws-parameter-store"
|
||||
| "aws-secret-manager"
|
||||
| "heroku"
|
||||
| "vercel"
|
||||
| "netlify"
|
||||
| "github"
|
||||
| "gitlab"
|
||||
| "render"
|
||||
| "railway"
|
||||
| "flyio"
|
||||
| "circleci"
|
||||
| "travisci"
|
||||
| "supabase"
|
||||
| "checkly"
|
||||
| "hashicorp-vault";
|
||||
integrationAuth: Types.ObjectId;
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
url: {
|
||||
// for custom self-hosted integrations (e.g. self-hosted GitHub enterprise)
|
||||
type: String,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
app: {
|
||||
// name of app in provider
|
||||
@@ -90,17 +91,17 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
},
|
||||
targetEnvironmentId: {
|
||||
type: String,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
targetService: {
|
||||
// railway-specific service
|
||||
type: String,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
targetServiceId: {
|
||||
// railway-specific service
|
||||
type: String,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
owner: {
|
||||
// github-specific repo owner-login
|
||||
@@ -111,12 +112,12 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
// aws-parameter-store-specific path
|
||||
// (also) vercel preview-branch
|
||||
type: String,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
region: {
|
||||
// aws-parameter-store-specific path
|
||||
type: String,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
integration: {
|
||||
type: String,
|
||||
@@ -136,7 +137,7 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
INTEGRATION_TRAVISCI,
|
||||
INTEGRATION_SUPABASE,
|
||||
INTEGRATION_CHECKLY,
|
||||
INTEGRATION_HASHICORP_VAULT
|
||||
INTEGRATION_HASHICORP_VAULT,
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
@@ -145,6 +146,11 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
ref: "IntegrationAuth",
|
||||
required: true,
|
||||
},
|
||||
secretPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "/",
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -1,75 +1,77 @@
|
||||
import express from 'express';
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireIntegrationAuth,
|
||||
requireIntegrationAuthorizationAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
requireAuth,
|
||||
requireIntegrationAuth,
|
||||
requireIntegrationAuthorizationAuth,
|
||||
validateRequest,
|
||||
} from "../../middleware";
|
||||
import {
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY
|
||||
} from '../../variables';
|
||||
import { body, param } from 'express-validator';
|
||||
import { integrationController } from '../../controllers/v1';
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
} from "../../variables";
|
||||
import { body, param } from "express-validator";
|
||||
import { integrationController } from "../../controllers/v1";
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
|
||||
}),
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: 'body'
|
||||
}),
|
||||
body('integrationAuthId').exists().isString().trim(),
|
||||
body('app').trim(),
|
||||
body('isActive').exists().isBoolean(),
|
||||
body('appId').trim(),
|
||||
body('sourceEnvironment').trim(),
|
||||
body('targetEnvironment').trim(),
|
||||
body('targetEnvironmentId').trim(),
|
||||
body('targetService').trim(),
|
||||
body('targetServiceId').trim(),
|
||||
body('owner').trim(),
|
||||
body('path').trim(),
|
||||
body('region').trim(),
|
||||
validateRequest,
|
||||
integrationController.createIntegration
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
|
||||
}),
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
location: "body",
|
||||
}),
|
||||
body("integrationAuthId").exists().isString().trim(),
|
||||
body("app").trim(),
|
||||
body("isActive").exists().isBoolean(),
|
||||
body("appId").trim(),
|
||||
body("secretPath").default("/").isString().trim(),
|
||||
body("sourceEnvironment").trim(),
|
||||
body("targetEnvironment").trim(),
|
||||
body("targetEnvironmentId").trim(),
|
||||
body("targetService").trim(),
|
||||
body("targetServiceId").trim(),
|
||||
body("owner").trim(),
|
||||
body("path").trim(),
|
||||
body("region").trim(),
|
||||
validateRequest,
|
||||
integrationController.createIntegration
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:integrationId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireIntegrationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('integrationId').exists().trim(),
|
||||
body('isActive').exists().isBoolean(),
|
||||
body('app').exists().trim(),
|
||||
body('environment').exists().trim(),
|
||||
body('appId').exists(),
|
||||
body('targetEnvironment').exists(),
|
||||
body('owner').exists(),
|
||||
validateRequest,
|
||||
integrationController.updateIntegration
|
||||
"/:integrationId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
requireIntegrationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
}),
|
||||
param("integrationId").exists().trim(),
|
||||
body("isActive").exists().isBoolean(),
|
||||
body("app").exists().trim(),
|
||||
body("secretPath").default("/").isString().trim(),
|
||||
body("environment").exists().trim(),
|
||||
body("appId").exists(),
|
||||
body("targetEnvironment").exists(),
|
||||
body("owner").exists(),
|
||||
validateRequest,
|
||||
integrationController.updateIntegration
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:integrationId',
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireIntegrationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param('integrationId').exists().trim(),
|
||||
validateRequest,
|
||||
integrationController.deleteIntegration
|
||||
"/:integrationId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
requireIntegrationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
}),
|
||||
param("integrationId").exists().trim(),
|
||||
validateRequest,
|
||||
integrationController.deleteIntegration
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,110 +1,112 @@
|
||||
import { Types } from 'mongoose';
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
getSecretsBotHelper,
|
||||
encryptSymmetricHelper,
|
||||
decryptSymmetricHelper,
|
||||
getKey,
|
||||
getIsWorkspaceE2EEHelper
|
||||
} from '../helpers/bot';
|
||||
getSecretsBotHelper,
|
||||
encryptSymmetricHelper,
|
||||
decryptSymmetricHelper,
|
||||
getKey,
|
||||
getIsWorkspaceE2EEHelper,
|
||||
} from "../helpers/bot";
|
||||
|
||||
/**
|
||||
* Class to handle bot actions
|
||||
*/
|
||||
class BotService {
|
||||
|
||||
/**
|
||||
* Return whether or not workspace with id [workspaceId] is end-to-end encrypted
|
||||
* @param workspaceId - id of workspace
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) {
|
||||
return await getIsWorkspaceE2EEHelper(workspaceId);
|
||||
}
|
||||
/**
|
||||
* Return whether or not workspace with id [workspaceId] is end-to-end encrypted
|
||||
* @param workspaceId - id of workspace
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) {
|
||||
return await getIsWorkspaceE2EEHelper(workspaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workspace key for workspace with id [workspaceId] shared to bot.
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for
|
||||
* @returns
|
||||
*/
|
||||
static async getWorkspaceKeyWithBot({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
return await getKey({
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decrypted secrets for workspace with id [workspaceId] and
|
||||
* environment [environmen] shared to bot.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace of secrets
|
||||
* @param {String} obj.environment - environment for secrets
|
||||
* @returns {Object} secretObj - object where keys are secret keys and values are secret values
|
||||
*/
|
||||
static async getSecrets({
|
||||
workspaceId,
|
||||
environment
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
}) {
|
||||
return await getSecretsBotHelper({
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {String} obj.plaintext - plaintext to encrypt
|
||||
*/
|
||||
static async encryptSymmetric({
|
||||
workspaceId,
|
||||
plaintext
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
plaintext: string;
|
||||
}) {
|
||||
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
|
||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||
* @param {String} obj.iv - iv
|
||||
* @param {String} obj.tag - tag
|
||||
*/
|
||||
static async decryptSymmetric({
|
||||
workspaceId,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}) {
|
||||
return await decryptSymmetricHelper({
|
||||
workspaceId,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get workspace key for workspace with id [workspaceId] shared to bot.
|
||||
* @param {Object} obj
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for
|
||||
* @returns
|
||||
*/
|
||||
static async getWorkspaceKeyWithBot({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
return await getKey({
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decrypted secrets for workspace with id [workspaceId] and
|
||||
* environment [environmen] shared to bot.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace of secrets
|
||||
* @param {String} obj.environment - environment for secrets
|
||||
* @returns {Object} secretObj - object where keys are secret keys and values are secret values
|
||||
*/
|
||||
static async getSecrets({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}) {
|
||||
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
|
||||
* @param {String} obj.plaintext - plaintext to encrypt
|
||||
*/
|
||||
static async encryptSymmetric({
|
||||
workspaceId,
|
||||
plaintext,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
plaintext: string;
|
||||
}) {
|
||||
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
|
||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||
* @param {String} obj.iv - iv
|
||||
* @param {String} obj.tag - tag
|
||||
*/
|
||||
static async decryptSymmetric({
|
||||
workspaceId,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}) {
|
||||
return await decryptSymmetricHelper({
|
||||
workspaceId,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default BotService;
|
||||
export default BotService;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
BackupPrivateKey,
|
||||
IntegrationAuth,
|
||||
ServiceTokenData,
|
||||
Integration,
|
||||
} from "../../models";
|
||||
import { generateKeyPair } from "../../utils/crypto";
|
||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
|
||||
@@ -424,3 +425,19 @@ export const backfillServiceToken = async () => {
|
||||
);
|
||||
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");
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
backfillEncryptionMetadata,
|
||||
backfillSecretFolders,
|
||||
backfillServiceToken,
|
||||
backfillIntegration,
|
||||
} from "./backfillData";
|
||||
import {
|
||||
reencryptBotPrivateKeys,
|
||||
@@ -77,6 +78,7 @@ export const setup = async () => {
|
||||
await backfillEncryptionMetadata();
|
||||
await backfillSecretFolders();
|
||||
await backfillServiceToken();
|
||||
await backfillIntegration();
|
||||
|
||||
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||
|
||||
BIN
frontend/public/images/integrations/Terraform.png
Normal file
BIN
frontend/public/images/integrations/Terraform.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
@@ -17,6 +17,12 @@
|
||||
"image": "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",
|
||||
"slug": "react",
|
||||
|
||||
@@ -4,7 +4,11 @@ import { useRouter } from 'next/router';
|
||||
import { faArrowRight, faCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
// 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 ListBox from '@app/components/basic/Listbox';
|
||||
@@ -27,6 +31,7 @@ interface Integration {
|
||||
targetEnvironment: string;
|
||||
workspace: string;
|
||||
integrationAuth: string;
|
||||
secretPath: string;
|
||||
}
|
||||
|
||||
interface IntegrationApp {
|
||||
@@ -55,7 +60,6 @@ const IntegrationTile = ({
|
||||
environments = [],
|
||||
handleDeleteIntegration
|
||||
}: Props) => {
|
||||
|
||||
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
||||
environments.find(({ slug }) => slug === integration?.environment) || {
|
||||
name: '',
|
||||
@@ -74,16 +78,16 @@ const IntegrationTile = ({
|
||||
});
|
||||
|
||||
setApps(tempApps);
|
||||
|
||||
|
||||
if (integration?.app) {
|
||||
setIntegrationApp(integration.app);
|
||||
} else if (integration?.path && integration?.region) {
|
||||
setIntegrationApp(`${integration.path} (${integration.region})`);
|
||||
} else if (tempApps.length > 0) {
|
||||
setIntegrationApp(tempApps[0].name)
|
||||
} else {
|
||||
setIntegrationApp('');
|
||||
}
|
||||
setIntegrationApp(tempApps[0].name);
|
||||
} else {
|
||||
setIntegrationApp('');
|
||||
}
|
||||
|
||||
switch (integration.integration) {
|
||||
case 'vercel':
|
||||
@@ -212,12 +216,15 @@ const IntegrationTile = ({
|
||||
return <div />;
|
||||
};
|
||||
|
||||
if (!integrationApp && integration.integration !== "checkly") return <div />;
|
||||
|
||||
const isSelected = integration.integration === 'hashicorp-vault' ? `${integration.app} - path: ${integration.path}` : integrationApp;
|
||||
if (!integrationApp && integration.integration !== 'checkly') return <div />;
|
||||
|
||||
const isSelected =
|
||||
integration.integration === 'hashicorp-vault'
|
||||
? `${integration.app} - path: ${integration.path}`
|
||||
: integrationApp;
|
||||
|
||||
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>
|
||||
<p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p>
|
||||
@@ -235,33 +242,46 @@ const IntegrationTile = ({
|
||||
isFull
|
||||
/>
|
||||
</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">
|
||||
<FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" />
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<p className="text-gray-400 text-xs font-semibold mb-2">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">
|
||||
<p className="mb-2 text-xs font-semibold text-gray-400">INTEGRATION</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)} */}
|
||||
{integrationSlugNameMapping[integration.integration]}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
|
||||
{integrationApp ? <div title={integrationApp}>
|
||||
<ListBox
|
||||
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
||||
isSelected={isSelected}
|
||||
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>}
|
||||
{integrationApp ? (
|
||||
<div title={integrationApp}>
|
||||
<ListBox
|
||||
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
||||
isSelected={isSelected}
|
||||
onChange={(app) => {
|
||||
setIntegrationApp(app);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-10 w-52 animate-pulse rounded-md bg-mineshaft-600 px-4 py-2 font-bold">
|
||||
-
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{renderIntegrationSpecificParams(integration)}
|
||||
</div>
|
||||
<div className="flex items-end cursor-default">
|
||||
<div className="flex cursor-default items-end">
|
||||
{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" />
|
||||
<div className="font-semibold text-gray-300">In Sync</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ interface Integration {
|
||||
targetEnvironment: string;
|
||||
workspace: string;
|
||||
integrationAuth: string;
|
||||
secretPath: string;
|
||||
}
|
||||
|
||||
const ProjectIntegrationSection = ({
|
||||
|
||||
@@ -3,6 +3,7 @@ import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
interface Props {
|
||||
integrationAuthId: string;
|
||||
isActive: boolean;
|
||||
secretPath: string;
|
||||
app: string | null;
|
||||
appId: string | null;
|
||||
sourceEnvironment: string;
|
||||
@@ -20,19 +21,20 @@ interface Props {
|
||||
* @param {String} obj.accessToken - id of integration authorization for which to create the integration
|
||||
* @returns
|
||||
*/
|
||||
const createIntegration = ({
|
||||
integrationAuthId,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region
|
||||
const createIntegration = ({
|
||||
integrationAuthId,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
secretPath
|
||||
}: Props) =>
|
||||
SecurityClient.fetchCall('/api/v1/integration', {
|
||||
method: 'POST',
|
||||
@@ -40,18 +42,19 @@ const createIntegration = ({
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
integrationAuthId,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region
|
||||
integrationAuthId,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
secretPath
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
@@ -61,4 +64,4 @@ const createIntegration = ({
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default createIntegration;
|
||||
export default createIntegration;
|
||||
|
||||
@@ -44,6 +44,7 @@ interface Integration {
|
||||
integration: string;
|
||||
targetEnvironment: string;
|
||||
workspace: string;
|
||||
secretPath:string;
|
||||
integrationAuth: string;
|
||||
}
|
||||
|
||||
@@ -436,7 +437,15 @@ export default function Integrations() {
|
||||
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} />
|
||||
</div>
|
||||
|
||||
@@ -56,6 +56,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [selectedAWSRegion, setSelectedAWSRegion] = useState('');
|
||||
const [path, setPath] = useState('');
|
||||
const [pathErrorText, setPathErrorText] = useState('');
|
||||
@@ -69,9 +70,9 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
}
|
||||
}, [workspace]);
|
||||
|
||||
const isValidAWSParameterStorePath = (secretPath: string) => {
|
||||
const isValidAWSParameterStorePath = (awsStorePath: string) => {
|
||||
const pattern = /^\/([\w-]+\/)*[\w-]+\/$/;
|
||||
return pattern.test(secretPath) && secretPath.length <= 2048;
|
||||
return pattern.test(awsStorePath) && awsStorePath.length <= 2048;
|
||||
};
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
@@ -101,7 +102,8 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path,
|
||||
region: selectedAWSRegion
|
||||
region: selectedAWSRegion,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -133,6 +135,13 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Select
|
||||
value={selectedAWSRegion}
|
||||
|
||||
@@ -56,6 +56,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [selectedAWSRegion, setSelectedAWSRegion] = useState('');
|
||||
const [targetSecretName, setTargetSecretName] = useState('');
|
||||
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState('');
|
||||
@@ -100,7 +101,8 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: selectedAWSRegion
|
||||
region: selectedAWSRegion,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -132,6 +134,13 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Select
|
||||
value={selectedAWSRegion}
|
||||
|
||||
@@ -24,6 +24,7 @@ export default function AzureKeyVaultCreateIntegrationPage() {
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
|
||||
const [vaultBaseUrl, setVaultBaseUrl] = useState('');
|
||||
const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState('');
|
||||
@@ -63,7 +64,8 @@ export default function AzureKeyVaultCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -93,6 +95,13 @@ export default function AzureKeyVaultCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Vault URI"
|
||||
errorText={vaultBaseUrlErrorText}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
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 {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -22,6 +30,8 @@ export default function ChecklyCreateIntegrationPage() {
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
const [targetAppId, setTargetAppId] = useState('');
|
||||
|
||||
@@ -63,7 +73,8 @@ export default function ChecklyCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -80,8 +91,13 @@ export default function ChecklyCreateIntegrationPage() {
|
||||
integrationAuthApps &&
|
||||
targetApp ? (
|
||||
<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">
|
||||
<CardTitle className="text-left px-6" subTitle="Choose which environment in Infisical you want to sync with your Checkly account.">Checkly Integration</CardTitle>
|
||||
<Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
|
||||
<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">
|
||||
<Select
|
||||
value={selectedSourceEnvironment}
|
||||
@@ -98,6 +114,13 @@ export default function ChecklyCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
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 {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -22,6 +30,8 @@ export default function CircleCICreateIntegrationPage() {
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -62,7 +72,8 @@ export default function CircleCICreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -97,6 +108,13 @@ export default function CircleCICreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
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 {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -22,6 +30,8 @@ export default function FlyioCreateIntegrationPage() {
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -61,7 +71,8 @@ export default function FlyioCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -96,6 +107,13 @@ export default function FlyioCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
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 {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -22,6 +30,7 @@ export default function GitHubCreateIntegrationPage() {
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [targetAppId, setTargetAppId] = useState('');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -66,7 +75,8 @@ export default function GitHubCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: targetApp.owner,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -100,6 +110,13 @@ export default function GitHubCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Select
|
||||
value={targetAppId}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
Select,
|
||||
SelectItem
|
||||
} from '../../../components/v2';
|
||||
import {
|
||||
@@ -44,6 +44,7 @@ export default function GitLabCreateIntegrationPage() {
|
||||
|
||||
const [targetEntity, setTargetEntity] = useState(gitLabEntities[0].value);
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [targetAppId, setTargetAppId] = useState('');
|
||||
const [targetEnvironment, setTargetEnvironment] = useState('');
|
||||
|
||||
@@ -101,7 +102,8 @@ export default function GitLabCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -136,6 +138,13 @@ export default function GitLabCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Select
|
||||
value={targetEntity}
|
||||
@@ -198,13 +207,11 @@ export default function GitLabCreateIntegrationPage() {
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="GitLab Environment Scope (Optional)"
|
||||
>
|
||||
<Input
|
||||
placeholder="*"
|
||||
value={targetEnvironment}
|
||||
onChange={(e) => setTargetEnvironment(e.target.value)}
|
||||
<FormControl label="GitLab Environment Scope (Optional)">
|
||||
<Input
|
||||
placeholder="*"
|
||||
value={targetEnvironment}
|
||||
onChange={(e) => setTargetEnvironment(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
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 { useGetWorkspaceById } from '../../../hooks/api/workspace';
|
||||
import createIntegration from '../../api/integrations/createIntegration';
|
||||
@@ -16,60 +24,58 @@ export default function HashiCorpVaultCreateIntegrationPage() {
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
|
||||
|
||||
const [vaultEnginePath, setVaultEnginePath] = useState('');
|
||||
const [vaultEnginePathErrorText, setVaultEnginePathErrorText ] = useState('');
|
||||
const [vaultEnginePathErrorText, setVaultEnginePathErrorText] = useState('');
|
||||
|
||||
const [vaultSecretPath, setVaultSecretPath] = useState('');
|
||||
const [vaultSecretPathErrorText, setVaultSecretPathErrorText] = useState('');
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const isValidVaultPath = (secretPath: string) => {
|
||||
return !(
|
||||
secretPath.length === 0 ||
|
||||
secretPath.startsWith('/') ||
|
||||
secretPath.endsWith('/')
|
||||
);
|
||||
const isValidVaultPath = (vaultPath: string) => {
|
||||
return !(vaultPath.length === 0 || vaultPath.startsWith('/') || vaultPath.endsWith('/'));
|
||||
};
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
if (!integrationAuth?._id) return;
|
||||
if (!integrationAuth?._id) return;
|
||||
|
||||
if (!isValidVaultPath(vaultEnginePath)) {
|
||||
setVaultEnginePathErrorText('Vault KV Secrets Engine Path must be valid like kv');
|
||||
} else {
|
||||
setVaultEnginePathErrorText('');
|
||||
}
|
||||
|
||||
if (!isValidVaultPath(vaultSecretPath)) {
|
||||
setVaultSecretPathErrorText('Vault Secret(s) Path must be valid like machine/dev');
|
||||
} else {
|
||||
setVaultSecretPathErrorText('');
|
||||
}
|
||||
|
||||
if (!isValidVaultPath || !isValidVaultPath(vaultSecretPath)) return;
|
||||
if (!isValidVaultPath(vaultEnginePath)) {
|
||||
setVaultEnginePathErrorText('Vault KV Secrets Engine Path must be valid like kv');
|
||||
} else {
|
||||
setVaultEnginePathErrorText('');
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
if (!isValidVaultPath(vaultSecretPath)) {
|
||||
setVaultSecretPathErrorText('Vault Secret(s) Path must be valid like machine/dev');
|
||||
} else {
|
||||
setVaultSecretPathErrorText('');
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error(err);
|
||||
}
|
||||
@@ -95,34 +101,41 @@ export default function HashiCorpVaultCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Vault KV Secrets Engine Path"
|
||||
errorText={vaultEnginePathErrorText}
|
||||
isError={vaultEnginePathErrorText !== '' ?? false}
|
||||
>
|
||||
<Input
|
||||
placeholder="kv"
|
||||
value={vaultEnginePath}
|
||||
onChange={(e) => setVaultEnginePath(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="kv"
|
||||
value={vaultEnginePath}
|
||||
onChange={(e) => setVaultEnginePath(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Vault Secret(s) Path"
|
||||
errorText={vaultSecretPathErrorText}
|
||||
isError={vaultSecretPathErrorText !== '' ?? false}
|
||||
>
|
||||
<Input
|
||||
placeholder="machine/dev"
|
||||
value={vaultSecretPath}
|
||||
onChange={(e) => setVaultSecretPath(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="machine/dev"
|
||||
value={vaultSecretPath}
|
||||
onChange={(e) => setVaultSecretPath(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
isDisabled={!(isValidVaultPath(vaultEnginePath) && isValidVaultPath(vaultSecretPath))}
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
isDisabled={!(isValidVaultPath(vaultEnginePath) && isValidVaultPath(vaultSecretPath))}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
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 {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -23,6 +31,7 @@ export default function HerokuCreateIntegrationPage() {
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -60,7 +69,8 @@ export default function HerokuCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -94,6 +104,13 @@ export default function HerokuCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
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 {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -31,7 +39,7 @@ export default function NetlifyCreateIntegrationPage() {
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
const [targetEnvironment, setTargetEnvironment] = useState('');
|
||||
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -71,7 +79,8 @@ export default function NetlifyCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -106,6 +115,13 @@ export default function NetlifyCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
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 {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById,
|
||||
@@ -20,6 +28,7 @@ export default function RailwayCreateIntegrationPage() {
|
||||
const [targetServiceId, setTargetServiceId] = useState('');
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
|
||||
@@ -99,7 +108,8 @@ export default function RailwayCreateIntegrationPage() {
|
||||
targetServiceId: targetService ? targetService.serviceId : null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -134,6 +144,13 @@ export default function RailwayCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Select
|
||||
value={targetAppId}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
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 {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -23,7 +31,7 @@ export default function RenderCreateIntegrationPage() {
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -62,7 +70,8 @@ export default function RenderCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -97,6 +106,13 @@ export default function RenderCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
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 {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -22,6 +30,7 @@ export default function SupabaseCreateIntegrationPage() {
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -62,7 +71,8 @@ export default function SupabaseCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -97,6 +107,13 @@ export default function SupabaseCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
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 {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -23,7 +31,7 @@ export default function TravisCICreateIntegrationPage() {
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -62,7 +70,8 @@ export default function TravisCICreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -97,6 +106,13 @@ export default function TravisCICreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
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 {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById,
|
||||
@@ -21,6 +29,7 @@ export default function VercelCreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [targetAppId, setTargetAppId] = useState('');
|
||||
const [targetEnvironment, setTargetEnvironment] = useState('');
|
||||
const [targetBranch, setTargetBranch] = useState('');
|
||||
@@ -84,7 +93,8 @@ export default function VercelCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -119,6 +129,13 @@ export default function VercelCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Select
|
||||
value={targetAppId}
|
||||
|
||||
Reference in New Issue
Block a user