Files
infisical/frontend/src/pages/integrations/[id].tsx
2023-03-09 11:52:34 +07:00

414 lines
15 KiB
TypeScript

import crypto from 'crypto';
import { useEffect, useState } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import frameworkIntegrationOptions from 'public/json/frameworkIntegrations.json';
import ActivateBotDialog from '@app/components/basic/dialog/ActivateBotDialog';
import CloudIntegrationSection from '@app/components/integrations/CloudIntegrationSection';
import FrameworkIntegrationSection from '@app/components/integrations/FrameworkIntegrationSection';
import IntegrationSection from '@app/components/integrations/IntegrationSection';
import NavHeader from '@app/components/navigation/NavHeader';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
import {
decryptAssymmetric,
encryptAssymmetric
} from '../../components/utilities/cryptography/crypto';
import getBot from '../api/bot/getBot';
import setBotActiveStatus from '../api/bot/setBotActiveStatus';
import deleteIntegration from '../api/integrations/DeleteIntegration';
import getIntegrationOptions from '../api/integrations/GetIntegrationOptions';
import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations';
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
import getAWorkspace from '../api/workspace/getAWorkspace';
import getLatestFileKey from '../api/workspace/getLatestFileKey';
interface IntegrationAuth {
_id: string;
integration: string;
workspace: string;
createdAt: string;
updatedAt: string;
}
interface Integration {
_id: string;
isActive: boolean;
app: string | null;
appId: string | null;
createdAt: string;
updatedAt: string;
environment: string;
integration: string;
targetEnvironment: string;
workspace: string;
integrationAuth: string;
}
interface IntegrationOption {
tenantId?: string;
clientId: string;
clientSlug?: string; // vercel-integration specific
docsLink: string;
image: string;
isAvailable: boolean;
name: string;
slug: string;
type: string;
}
export default function Integrations() {
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
const [integrationAuths, setIntegrationAuths] = useState<IntegrationAuth[]>([]);
const [environments, setEnvironments] = useState<
{
name: string;
slug: string;
}[]
>([]);
const [integrations, setIntegrations] = useState<Integration[]>([]);
// TODO: These will have its type when migratiing towards react-query
const [bot, setBot] = useState<any>(null);
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState<IntegrationOption | null>(null);
const router = useRouter();
const workspaceId = router.query.id as string;
const { t } = useTranslation();
useEffect(() => {
(async () => {
try {
const workspace = await getAWorkspace(workspaceId);
setEnvironments(workspace.environments);
// get cloud integration options
setCloudIntegrationOptions(await getIntegrationOptions());
// get project integration authorizations
setIntegrationAuths(
await getWorkspaceAuthorizations({
workspaceId
})
);
// get project integrations
setIntegrations(
await getWorkspaceIntegrations({
workspaceId
})
);
// get project bot
setBot(await getBot({ workspaceId }));
} catch (err) {
console.error(err);
}
})();
}, []);
/**
* Activate bot for project by performing the following steps:
* 1. Get the (encrypted) project key
* 2. Decrypt project key with user's private key
* 3. Encrypt project key with bot's public key
* 4. Send encrypted project key to backend and set bot status to active
*/
const handleBotActivate = async () => {
let botKey;
try {
if (bot) {
// case: there is a bot
const key = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
if (!PRIVATE_KEY) {
throw new Error('Private Key missing');
}
const WORKSPACE_KEY = decryptAssymmetric({
ciphertext: key.latestKey.encryptedKey,
nonce: key.latestKey.nonce,
publicKey: key.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: WORKSPACE_KEY,
publicKey: bot.publicKey,
privateKey: PRIVATE_KEY
});
botKey = {
encryptedKey: ciphertext,
nonce
};
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: true,
botKey
})
).bot
);
}
} catch (err) {
console.error(err);
}
};
const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
try {
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString('hex');
localStorage.setItem('latestCSRFToken', state);
let link = '';
switch (integrationOption.slug) {
case 'azure-key-vault':
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
break;
case 'aws-parameter-store':
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
break;
case 'aws-secret-manager':
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
break;
case 'heroku':
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
break;
case 'vercel':
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break;
case 'netlify':
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break;
case 'github':
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case 'gitlab':
link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
break;
case 'render':
link = `${window.location.origin}/integrations/render/authorize`;
break;
case 'flyio':
link = `${window.location.origin}/integrations/flyio/authorize`;
break;
case 'circleci':
link = `${window.location.origin}/integrations/circleci/authorize`;
break;
case 'travisci':
link = `${window.location.origin}/integrations/travisci/authorize`;
break;
default:
break;
}
if (link !== '') {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
}
const handleAuthorizedIntegrationOptionPress = (integrationAuth: IntegrationAuth) => {
try {
let link = '';
switch (integrationAuth.integration) {
case 'azure-key-vault':
link = `${window.location.origin}/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'aws-parameter-store':
link = `${window.location.origin}/integrations/aws-parameter-store/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'aws-secret-manager':
link = `${window.location.origin}/integrations/aws-secret-manager/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'heroku':
link = `${window.location.origin}/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'vercel':
link = `${window.location.origin}/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'netlify':
link = `${window.location.origin}/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'github':
link = `${window.location.origin}/integrations/github/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'gitlab':
link = `${window.location.origin}/integrations/gitlab/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'render':
link = `${window.location.origin}/integrations/render/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'flyio':
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'circleci':
link = `${window.location.origin}/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'travisci':
link = `${window.location.origin}/integrations/travisci/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}
if (link !== '') {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
}
/**
* Open dialog to activate bot if bot is not active.
* Otherwise, start integration [integrationOption]
* @param {Object} integrationOption - an integration option
* @param {String} integrationOption.name
* @param {String} integrationOption.type
* @param {String} integrationOption.docsLink
* @returns
*/
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
try {
const integrationAuthX = integrationAuths.find((integrationAuth) => integrationAuth.integration === integrationOption.slug);
if (!bot.isActive) {
await handleBotActivate();
}
if (!integrationAuthX) {
// case: integration has not been authorized
handleUnauthorizedIntegrationOptionPress(integrationOption);
return;
}
handleAuthorizedIntegrationOptionPress(integrationAuthX);
} catch (err) {
console.error(err);
}
};
/**
* Handle deleting integration authorization [integrationAuth] and corresponding integrations from state where applicable
* @param {Object} obj
* @param {IntegrationAuth} obj.integrationAuth - integrationAuth to delete
*/
const handleDeleteIntegrationAuth = async ({ integrationAuth: deletedIntegrationAuth }: { integrationAuth: IntegrationAuth }) => {
try {
const newIntegrations = integrations.filter((integration) => integration.integrationAuth !== deletedIntegrationAuth._id);
setIntegrationAuths(integrationAuths.filter((integrationAuth) => integrationAuth._id !== deletedIntegrationAuth._id));
setIntegrations(newIntegrations);
// handle updating bot
if (newIntegrations.length < 1) {
// case: no integrations left
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: false
})
).bot
);
}
} catch (err) {
console.error(err);
}
}
/**
* Handle deleting integration [integration]
* @param {Object} obj
* @param {Integration} obj.integration - integration to delete
*/
const handleDeleteIntegration = async ({ integration }: { integration: Integration }) => {
try {
const deletedIntegration = await deleteIntegration({
integrationId: integration._id
});
const newIntegrations = integrations.filter((i) => i._id !== deletedIntegration._id);
setIntegrations(newIntegrations);
// handle updating bot
if (newIntegrations.length < 1) {
// case: no integrations left
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: false
})
).bot
);
}
} catch (err) {
console.error(err);
}
}
return (
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
<Head>
<title>{t('common:head-title', { title: t('integrations:title') })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content="Manage your .env files in seconds" />
<meta name="og:description" content={t('integrations:description') as string} />
</Head>
<div className="w-full pb-2 h-screen max-h-[calc(100vh-10px)] overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
<NavHeader pageName={t('integrations:title')} isProjectRelated />
<ActivateBotDialog
isOpen={isActivateBotDialogOpen}
closeModal={() => setIsActivateBotDialogOpen(false)}
selectedIntegrationOption={selectedIntegrationOption}
integrationOptionPress={integrationOptionPress}
/>
<IntegrationSection
integrations={integrations}
setIntegrations={setIntegrations}
bot={bot}
setBot={setBot}
environments={environments}
handleDeleteIntegration={handleDeleteIntegration}
/>
{cloudIntegrationOptions.length > 0 && bot ? (
<CloudIntegrationSection
cloudIntegrationOptions={cloudIntegrationOptions}
setSelectedIntegrationOption={setSelectedIntegrationOption as any}
integrationOptionPress={(integrationOption: IntegrationOption) => {
if (!bot.isActive) {
// case: bot is not active -> open modal to activate bot
setIsActivateBotDialogOpen(true);
return;
}
integrationOptionPress(integrationOption)
}}
integrationAuths={integrationAuths}
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
/>
) : (
<div />
)}
<FrameworkIntegrationSection frameworks={frameworkIntegrationOptions as any} />
</div>
</div>
);
}
Integrations.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);