Add Render, Fly.io integrations and reduce integrations page reloads

This commit is contained in:
Tuan Dang
2023-01-22 00:09:37 +07:00
parent 06803519e6
commit 0c351c0925
45 changed files with 1096 additions and 350 deletions
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

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