mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 12:29:26 +00:00
Merge branch 'bot'
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import getLatestFileKey from "../../../pages/api/workspace/getLatestFileKey";
|
||||
import setBotActiveStatus from "../../../pages/api/bot/setBotActiveStatus";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "../../utilities/cryptography/crypto";
|
||||
import Button from "../buttons/Button";
|
||||
|
||||
const ActivateBotDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
selectedIntegrationOption,
|
||||
handleBotActivate,
|
||||
handleIntegrationOption
|
||||
}) => {
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
// 1. activate bot
|
||||
await handleBotActivate();
|
||||
|
||||
// 2. start integration
|
||||
await handleIntegrationOption({
|
||||
integrationOption: selectedIntegrationOption
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
closeModal();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
Grant Infisical access to your secrets
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 max-w-max">
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
color="mineshaft"
|
||||
text="Grant access"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActivateBotDialog;
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import getLatestFileKey from "../../../pages/api/workspace/getLatestFileKey";
|
||||
import setBotActiveStatus from "../../../pages/api/bot/setBotActiveStatus";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "../../utilities/cryptography/crypto";
|
||||
import Button from "../buttons/Button";
|
||||
import InputField from "../InputField";
|
||||
|
||||
const IntegrationAccessTokenDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
selectedIntegrationOption,
|
||||
handleBotActivate,
|
||||
handleIntegrationOption
|
||||
}) => {
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
// 1. activate bot
|
||||
await handleBotActivate();
|
||||
|
||||
// 2. start integration
|
||||
await handleIntegrationOption({
|
||||
integrationOption: selectedIntegrationOption
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
closeModal();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
Grant Infisical access to your secrets
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 max-w-max">
|
||||
{/* <Button
|
||||
onButtonPressed={submit}
|
||||
color="mineshaft"
|
||||
text="Grant access"
|
||||
size="md"
|
||||
/> */}
|
||||
<InputField
|
||||
label="Access token"
|
||||
onChangeHandler={() => {}}
|
||||
type="varName"
|
||||
value={"Hello"}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default IntegrationAccessTokenDialog;
|
||||
@@ -0,0 +1,121 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faCheck,
|
||||
faX,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import deleteIntegrationAuth from "../../pages/api/integrations/DeleteIntegrationAuth";
|
||||
|
||||
interface CloudIntegrationOption {
|
||||
isAvailable: Boolean;
|
||||
name: string;
|
||||
type: string;
|
||||
clientId: string;
|
||||
docsLink: string;
|
||||
}
|
||||
|
||||
interface IntegrationAuth {
|
||||
id: string;
|
||||
integration: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
cloudIntegrationOption: CloudIntegrationOption;
|
||||
setSelectedIntegrationOption: () => void;
|
||||
integrationOptionPress: () => void;
|
||||
integrationAuths: IntegrationAuth[];
|
||||
}
|
||||
|
||||
const CloudIntegration = ({
|
||||
cloudIntegrationOption,
|
||||
setSelectedIntegrationOption,
|
||||
integrationOptionPress,
|
||||
integrationAuths
|
||||
}: Props) => {
|
||||
const router = useRouter();
|
||||
return integrationAuths ? (
|
||||
<div
|
||||
className={`relative ${
|
||||
cloudIntegrationOption.isAvailable
|
||||
? "hover:bg-white/10 duration-200 cursor-pointer"
|
||||
: "opacity-50"
|
||||
} flex flex-row bg-white/5 h-32 rounded-md p-4 items-center`}
|
||||
onClick={() => {
|
||||
if (!cloudIntegrationOption.isAvailable) return;
|
||||
setSelectedIntegrationOption(cloudIntegrationOption);
|
||||
integrationOptionPress({
|
||||
integrationOption: cloudIntegrationOption
|
||||
});
|
||||
}}
|
||||
key={cloudIntegrationOption.name}
|
||||
>
|
||||
<Image
|
||||
src={`/images/integrations/${cloudIntegrationOption.name}.png`}
|
||||
height={70}
|
||||
width={70}
|
||||
alt="integration logo"
|
||||
/>
|
||||
{cloudIntegrationOption.name.split(" ").length > 2 ? (
|
||||
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs">
|
||||
<div>{cloudIntegrationOption.name.split(" ")[0]}</div>
|
||||
<div className="text-base">
|
||||
{cloudIntegrationOption.name.split(" ")[1]}{" "}
|
||||
{cloudIntegrationOption.name.split(" ")[2]}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs">
|
||||
{cloudIntegrationOption.name}
|
||||
</div>
|
||||
)}
|
||||
{cloudIntegrationOption.isAvailable &&
|
||||
integrationAuths
|
||||
.map((authorization) => authorization.integration)
|
||||
.includes(cloudIntegrationOption.name.toLowerCase()) && (
|
||||
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
||||
<div
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
deleteIntegrationAuth({
|
||||
integrationAuthId: integrationAuths
|
||||
.filter(
|
||||
(authorization) =>
|
||||
authorization.integration ==
|
||||
cloudIntegrationOption.name.toLowerCase()
|
||||
)
|
||||
.map((authorization) => authorization._id)[0],
|
||||
});
|
||||
|
||||
router.reload();
|
||||
}}
|
||||
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"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faX}
|
||||
className="text-xs mr-2 py-px"
|
||||
/>
|
||||
Revoke
|
||||
</div>
|
||||
<div className="w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className="text-xs mr-2"
|
||||
/>
|
||||
Authorized
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!cloudIntegrationOption.isAvailable && (
|
||||
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
||||
<div className="w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90">
|
||||
Coming Soon
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : <div></div>
|
||||
}
|
||||
|
||||
export default CloudIntegration;
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import CloudIntegration from "./CloudIntegration";
|
||||
|
||||
interface CloudIntegrationOption {
|
||||
name: string;
|
||||
type: string;
|
||||
clientId: string;
|
||||
docsLink: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
cloudIntegrationOptions: CloudIntegrationOption[];
|
||||
setSelectedIntegrationOption: () => void;
|
||||
integrationOptionPress: () => void;
|
||||
integrationAuths: any;
|
||||
}
|
||||
|
||||
const CloudIntegrationSection = ({
|
||||
cloudIntegrationOptions,
|
||||
setSelectedIntegrationOption,
|
||||
integrationOptionPress,
|
||||
integrationAuths
|
||||
}: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div className={`flex flex-col justify-between items-start m-4 mt-7 text-xl max-w-5xl px-2`}>
|
||||
<h1 className="font-semibold text-3xl">Cloud Integrations</h1>
|
||||
<p className="text-base text-gray-400">
|
||||
Click on an integration to begin syncing secrets to it.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 grid-cols-4 grid-rows-2 mx-6 max-w-5xl">
|
||||
{cloudIntegrationOptions.map((cloudIntegrationOption) => (
|
||||
<CloudIntegration
|
||||
cloudIntegrationOption={cloudIntegrationOption}
|
||||
setSelectedIntegrationOption={setSelectedIntegrationOption}
|
||||
integrationOptionPress={integrationOptionPress}
|
||||
integrationAuths={integrationAuths}
|
||||
key={`cloud-integration-${cloudIntegrationOption.slug}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CloudIntegrationSection;
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
interface Framework {
|
||||
name: string;
|
||||
slug: string;
|
||||
image: string;
|
||||
docsLink: string;
|
||||
}
|
||||
|
||||
const FrameworkIntegration = ({
|
||||
framework
|
||||
}: {
|
||||
framework: Framework;
|
||||
}) => {
|
||||
return (
|
||||
<a
|
||||
href={framework.docsLink}
|
||||
rel="noopener"
|
||||
className={`relative flex flex-row items-center justify-center bg-bunker-500 hover:bg-gradient-to-tr duration-200 h-32 rounded-md p-0.5 items-center cursor-pointer`}
|
||||
>
|
||||
<div className={`hover:bg-white/10 duration-200 cursor-pointer font-semibold bg-bunker-500 flex flex-col items-center justify-center h-full w-full rounded-md text-gray-300 group-hover:text-gray-200 duration-200 ${framework?.name?.split(" ").length > 1 ? "text-sm px-1" : "text-xl px-2"} text-center w-full max-w-xs`}>
|
||||
{framework?.image && <Image
|
||||
src={`/images/integrations/${framework.image}.png`}
|
||||
height={framework?.name ? 60 : 90}
|
||||
width={framework?.name ? 60 : 90}
|
||||
alt="integration logo"
|
||||
></Image>}
|
||||
{framework?.name && framework?.image && <div className="h-2"></div>}
|
||||
{framework?.name && framework.name}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default FrameworkIntegration;
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import FrameworkIntegration from "./FrameworkIntegration";
|
||||
|
||||
interface Framework {
|
||||
name: string;
|
||||
image: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
framework: Framework
|
||||
}
|
||||
|
||||
const FrameworkIntegrationSection = ({ frameworks }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2">
|
||||
<h1 className="font-semibold text-3xl">Framework Integrations</h1>
|
||||
<p className="text-base text-gray-400">
|
||||
Click on a framework to get the setup instructions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 grid-cols-7 grid-rows-2 mx-6 mt-4 max-w-5xl">
|
||||
{frameworks.map((framework) => (
|
||||
<FrameworkIntegration
|
||||
framework={framework}
|
||||
key={`framework-integration-${framework.slug}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default FrameworkIntegrationSection;
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faArrowRight,
|
||||
faRotate,
|
||||
faX,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
envMapping,
|
||||
reverseEnvMapping,
|
||||
reverseContextNetlifyMapping
|
||||
} from "../../public/data/frequentConstants";
|
||||
import updateIntegration from "../../pages/api/integrations/updateIntegration"
|
||||
import deleteIntegration from "../../pages/api/integrations/DeleteIntegration"
|
||||
import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps";
|
||||
import Button from "~/components/basic/buttons/Button";
|
||||
import ListBox from "~/components/basic/Listbox";
|
||||
|
||||
interface Integration {
|
||||
app?: string;
|
||||
environment: string;
|
||||
integration: string;
|
||||
integrationAuth: string;
|
||||
isActive: Boolean;
|
||||
}
|
||||
|
||||
const Integration = ({
|
||||
integration
|
||||
}: {
|
||||
integration: Integration;
|
||||
}) => {
|
||||
const [integrationEnvironment, setIntegrationEnvironment] = useState(
|
||||
reverseEnvMapping[integration.environment]
|
||||
);
|
||||
const [fileState, setFileState] = useState([]);
|
||||
const router = useRouter();
|
||||
const [apps, setApps] = useState([]); // integration app objects
|
||||
const [integrationApp, setIntegrationApp] = useState(null); // integration app name
|
||||
const [integrationTarget, setIntegrationTarget] = useState(null); // vercel-specific integration param
|
||||
const [integrationContext, setIntegrationContext] = useState(null); // netlify-specific integration param
|
||||
|
||||
useEffect(async () => {
|
||||
interface App {
|
||||
name: string;
|
||||
siteId?: string;
|
||||
}
|
||||
|
||||
const tempApps = await getIntegrationApps({
|
||||
integrationAuthId: integration.integrationAuth,
|
||||
});
|
||||
|
||||
setApps(tempApps);
|
||||
setIntegrationApp(
|
||||
integration.app ? integration.app : tempApps[0].name
|
||||
);
|
||||
|
||||
switch (integration.integration) {
|
||||
case "vercel":
|
||||
setIntegrationTarget("Development");
|
||||
break;
|
||||
case "netlify":
|
||||
setIntegrationContext("All");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderIntegrationSpecificParams = (integration) => {
|
||||
try {
|
||||
switch (integration.integration) {
|
||||
case "vercel":
|
||||
return (
|
||||
<div>
|
||||
<div className="text-gray-400 text-xs font-semibold mb-2">
|
||||
ENVIRONMENT
|
||||
</div>
|
||||
<ListBox
|
||||
data={!integration.isActive && [
|
||||
"Production",
|
||||
"Preview",
|
||||
"Development"
|
||||
]}
|
||||
selected={"Production"}
|
||||
onChange={setIntegrationTarget}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "netlify":
|
||||
return (
|
||||
<div>
|
||||
<div className="text-gray-400 text-xs font-semibold mb-2">
|
||||
CONTEXT
|
||||
</div>
|
||||
<ListBox
|
||||
data={!integration.isActive && [
|
||||
"All",
|
||||
"Production",
|
||||
"Deploy previews",
|
||||
"Branch deploys",
|
||||
"Local development"
|
||||
]}
|
||||
selected={integrationContext}
|
||||
onChange={setIntegrationContext}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <div></div>;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (!integrationApp || apps.length === 0) return <div></div>
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
|
||||
<div className="flex">
|
||||
<div>
|
||||
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p>
|
||||
<ListBox data={!integration.isActive ? [
|
||||
"Development",
|
||||
"Staging",
|
||||
"Testing",
|
||||
"Production",
|
||||
] : null}
|
||||
selected={integrationEnvironment}
|
||||
onChange={setIntegrationEnvironment}
|
||||
isFull={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRight}
|
||||
className="mx-4 text-gray-400 mt-8"
|
||||
/>
|
||||
</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">
|
||||
{integration.integration.charAt(0).toUpperCase() +
|
||||
integration.integration.slice(1)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<div className="text-gray-400 text-xs font-semibold mb-2">
|
||||
APP
|
||||
</div>
|
||||
<ListBox
|
||||
data={!integration.isActive && apps.map((app) => app.name)}
|
||||
selected={integrationApp}
|
||||
onChange={setIntegrationApp}
|
||||
/>
|
||||
</div>
|
||||
{renderIntegrationSpecificParams(integration)}
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
{integration.isActive ? (
|
||||
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
|
||||
<FontAwesomeIcon
|
||||
icon={faRotate}
|
||||
className="text-lg mr-2.5 text-primary animate-spin"
|
||||
/>
|
||||
<div className="text-gray-300 font-semibold">In Sync</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
text="Start Integration"
|
||||
onButtonPressed={async () => {
|
||||
const siteId = apps.find((app) => app.name === integrationApp).siteId ? apps.find((app) => app.name === integrationApp).siteId : null;
|
||||
const result = await updateIntegration({
|
||||
integrationId: integration._id,
|
||||
environment: envMapping[integrationEnvironment],
|
||||
app: integrationApp,
|
||||
isActive: true,
|
||||
target: integrationTarget ? integrationTarget.toLowerCase() : null,
|
||||
context: integrationContext ? reverseContextNetlifyMapping[integrationContext] : null,
|
||||
siteId
|
||||
});
|
||||
router.reload();
|
||||
}}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
|
||||
<Button
|
||||
onButtonPressed={async () => {
|
||||
await deleteIntegration({
|
||||
integrationId: integration._id,
|
||||
});
|
||||
router.reload();
|
||||
}}
|
||||
color="red"
|
||||
size="icon-md"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Integration;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import Integration from "./Integration";
|
||||
import guidGenerator from "~/utilities/randomId";
|
||||
|
||||
interface Integration {
|
||||
|
||||
}
|
||||
|
||||
interface Props {
|
||||
integrations: any
|
||||
}
|
||||
|
||||
const ProjectIntegrationSection = ({
|
||||
integrations
|
||||
}: Props) => {
|
||||
return integrations.length > 0 ? (
|
||||
<div className="mb-12">
|
||||
<div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2">
|
||||
<h1 className="font-semibold text-3xl">Current Integrations</h1>
|
||||
<p className="text-base text-gray-400">
|
||||
Manage your integrations of Infisical with third-party services.
|
||||
</p>
|
||||
</div>
|
||||
{integrations.map((integration => (
|
||||
<Integration
|
||||
key={guidGenerator()}
|
||||
integration={integration}
|
||||
/>
|
||||
)))}
|
||||
</div>
|
||||
) : <div></div>
|
||||
}
|
||||
|
||||
export default ProjectIntegrationSection;
|
||||
@@ -73,7 +73,7 @@ const attemptLogin = async (
|
||||
tag,
|
||||
privateKey
|
||||
});
|
||||
|
||||
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrgsData = userOrgs.map((org) => org._id);
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ const POSTHOG_HOST =
|
||||
process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
|
||||
const STRIPE_PRODUCT_PRO = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO!;
|
||||
const STRIPE_PRODUCT_STARTER = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER!;
|
||||
const CLIENT_ID_HEROKU = process.env.NEXT_PUBLIC_CLIENT_ID_HEROKU!;
|
||||
const CLIENT_ID_NETLIFY = process.env.NEXT_PUBLIC_CLIENT_ID_NETLIFY!;
|
||||
|
||||
export {
|
||||
ENV,
|
||||
@@ -11,4 +13,6 @@ export {
|
||||
POSTHOG_HOST,
|
||||
STRIPE_PRODUCT_PRO,
|
||||
STRIPE_PRODUCT_STARTER,
|
||||
};
|
||||
CLIENT_ID_HEROKU,
|
||||
CLIENT_ID_NETLIFY
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import SecurityClient from "~/utilities/SecurityClient";
|
||||
|
||||
interface Props {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the bot for a project
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId
|
||||
* @returns
|
||||
*/
|
||||
const getBot = async ({ workspaceId }: Props) => {
|
||||
return SecurityClient.fetchCall(
|
||||
"/api/v1/bot/" + workspaceId,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).bot;
|
||||
} else {
|
||||
console.log("Failed to get bot for project");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getBot;
|
||||
@@ -0,0 +1,46 @@
|
||||
import SecurityClient from "~/utilities/SecurityClient";
|
||||
|
||||
interface BotKey {
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
botId: string;
|
||||
isActive: Boolean;
|
||||
botKey: BotKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function sets the active status of a bot and shares a copy of
|
||||
* the project key (encrypted under the bot's public key) with the
|
||||
* project's bot
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.botId
|
||||
* @param {String} obj.isActive
|
||||
* @param {Object} obj.botKey
|
||||
* @returns
|
||||
*/
|
||||
const setBotActiveStatus = async ({ botId, isActive, botKey }: Props) => {
|
||||
return SecurityClient.fetchCall(
|
||||
"/api/v1/bot/" + botId + "/active",
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
isActive,
|
||||
botKey
|
||||
})
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return await res.json();
|
||||
} else {
|
||||
console.log("Failed to get bot for project");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default setBotActiveStatus;
|
||||
@@ -0,0 +1,21 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
const getIntegrationOptions = () => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/integration-auth/integration-options',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).integrationOptions;
|
||||
} else {
|
||||
console.log('Failed to get (cloud) integration options');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getIntegrationOptions;
|
||||
@@ -1,18 +0,0 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
const getIntegrations = () => {
|
||||
return SecurityClient.fetchCall('/api/v1/integration/integrations', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).integrations;
|
||||
} else {
|
||||
console.log('Failed to get project integrations');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getIntegrations;
|
||||
@@ -0,0 +1,51 @@
|
||||
import SecurityClient from "~/utilities/SecurityClient";
|
||||
|
||||
/**
|
||||
* This route starts the integration after teh default one if gonna set up.
|
||||
* Update integration with id [integrationId] to sync envars from the project's
|
||||
* [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
|
||||
* @returns
|
||||
*/
|
||||
const updateIntegration = ({
|
||||
integrationId,
|
||||
app,
|
||||
environment,
|
||||
isActive,
|
||||
target,
|
||||
context,
|
||||
siteId
|
||||
}) => {
|
||||
return SecurityClient.fetchCall(
|
||||
"/api/v1/integration/" + integrationId,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
app,
|
||||
environment,
|
||||
isActive,
|
||||
target,
|
||||
context,
|
||||
siteId
|
||||
}),
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log("Failed to start an integration");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default updateIntegration;
|
||||
@@ -413,29 +413,6 @@ export default function Dashboard() {
|
||||
setButtonReady(false);
|
||||
pushKeys({ obj, workspaceId: router.query.id, env });
|
||||
|
||||
/**
|
||||
* Check which integrations are active for this project and environment
|
||||
* If there are any, update environment variables for those integrations
|
||||
*/
|
||||
let integrations = await getWorkspaceIntegrations({
|
||||
workspaceId: router.query.id
|
||||
});
|
||||
integrations.map(async (integration) => {
|
||||
if (
|
||||
envMapping[env] == integration.environment &&
|
||||
integration.isActive == true
|
||||
) {
|
||||
let objIntegration = Object.assign(
|
||||
{},
|
||||
...data.map((row) => ({ [row.key]: row.value }))
|
||||
);
|
||||
await pushKeysIntegration({
|
||||
obj: objIntegration,
|
||||
integrationId: integration._id
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If this user has never saved environment variables before, show them a prompt to read docs
|
||||
if (!hasUserEverPushed) {
|
||||
setCheckDocsPopUpVisible(true);
|
||||
|
||||
@@ -16,16 +16,17 @@ export default function Heroku() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(async () => {
|
||||
try {
|
||||
if (state == localStorage.getItem("latestCSRFToken")) {
|
||||
if (state === localStorage.getItem('latestCSRFToken')) {
|
||||
localStorage.removeItem('latestCSRFToken');
|
||||
await AuthorizeIntegration({
|
||||
workspaceId: localStorage.getItem("projectData.id"),
|
||||
workspaceId: localStorage.getItem('projectData.id'),
|
||||
code,
|
||||
integration: "heroku",
|
||||
});
|
||||
router.push("/integrations/" + localStorage.getItem("projectData.id"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error - Not logged in yet");
|
||||
console.error('Heroku integration error: ', error);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
+199
-379
@@ -1,199 +1,178 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
faArrowRight,
|
||||
faCheck,
|
||||
faRotate,
|
||||
faX
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from '~/components/basic/buttons/Button';
|
||||
import ListBox from '~/components/basic/Listbox';
|
||||
import NavHeader from '~/components/navigation/NavHeader';
|
||||
import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject';
|
||||
import pushKeysIntegration from '~/components/utilities/secrets/pushKeysIntegration';
|
||||
import guidGenerator from '~/utilities/randomId';
|
||||
|
||||
import {
|
||||
envMapping,
|
||||
frameworks,
|
||||
reverseEnvMapping
|
||||
} from '../../public/data/frequentConstants';
|
||||
import deleteIntegration from '../api/integrations/DeleteIntegration';
|
||||
import deleteIntegrationAuth from '../api/integrations/DeleteIntegrationAuth';
|
||||
import getIntegrationApps from '../api/integrations/GetIntegrationApps';
|
||||
import getIntegrations from '../api/integrations/GetIntegrations';
|
||||
import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations';
|
||||
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
|
||||
import startIntegration from '../api/integrations/StartIntegration';
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
const Integration = ({ projectIntegration }) => {
|
||||
const [integrationEnvironment, setIntegrationEnvironment] = useState(
|
||||
reverseEnvMapping[projectIntegration.environment]
|
||||
);
|
||||
const [fileState, setFileState] = useState([]);
|
||||
const [data, setData] = useState();
|
||||
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
|
||||
const router = useRouter();
|
||||
const [apps, setApps] = useState([]);
|
||||
const [integrationApp, setIntegrationApp] = useState(
|
||||
projectIntegration.app ? projectIntegration.app : apps[0]
|
||||
);
|
||||
|
||||
useEffect(async () => {
|
||||
const tempHerokuApps = await getIntegrationApps({
|
||||
integrationAuthId: projectIntegration.integrationAuth
|
||||
});
|
||||
const tempHerokuAppNames = tempHerokuApps.map((app) => app.name);
|
||||
setApps(tempHerokuAppNames);
|
||||
setIntegrationApp(
|
||||
projectIntegration.app ? projectIntegration.app : tempHerokuAppNames[0]
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col max-w-5xl justify-center bg-white/5 p-6 rounded-md mx-6 mt-8">
|
||||
<div className="relative px-4 flex flex-row items-center justify-between mb-4">
|
||||
<div className="flex flex-row">
|
||||
<div>
|
||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
||||
ENVIRONMENT
|
||||
</div>
|
||||
<ListBox
|
||||
data={
|
||||
!projectIntegration.isActive && [
|
||||
'Development',
|
||||
'Staging',
|
||||
'Production'
|
||||
]
|
||||
}
|
||||
selected={integrationEnvironment}
|
||||
onChange={setIntegrationEnvironment}
|
||||
/>
|
||||
</div>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRight}
|
||||
className="mx-4 text-gray-400 mt-8"
|
||||
/>
|
||||
<div className="mr-2">
|
||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
||||
INTEGRATION
|
||||
</div>
|
||||
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-20 text-sm font-semibold text-gray-300">
|
||||
{projectIntegration.integration.charAt(0).toUpperCase() +
|
||||
projectIntegration.integration.slice(1)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
||||
HEROKU APP
|
||||
</div>
|
||||
<ListBox
|
||||
data={!projectIntegration.isActive && apps}
|
||||
selected={integrationApp}
|
||||
onChange={setIntegrationApp}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row mt-6">
|
||||
{projectIntegration.isActive ? (
|
||||
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
|
||||
<FontAwesomeIcon
|
||||
icon={faRotate}
|
||||
className="text-lg mr-2.5 text-primary animate-spin"
|
||||
/>
|
||||
<div className="text-gray-300 font-semibold">In Sync</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
text="Start Integration"
|
||||
onButtonPressed={async () => {
|
||||
const result = await startIntegration({
|
||||
integrationId: projectIntegration._id,
|
||||
environment: envMapping[integrationEnvironment],
|
||||
appName: integrationApp
|
||||
});
|
||||
if (result?.status == 200) {
|
||||
let currentSecrets = await getSecretsForProject({
|
||||
env: integrationEnvironment,
|
||||
setFileState,
|
||||
setIsKeyAvailable,
|
||||
setData,
|
||||
workspaceId: router.query.id
|
||||
});
|
||||
|
||||
let obj = Object.assign(
|
||||
{},
|
||||
...currentSecrets.map((row) => ({
|
||||
[row.key]: row.value
|
||||
}))
|
||||
);
|
||||
await pushKeysIntegration({
|
||||
obj,
|
||||
integrationId: projectIntegration._id
|
||||
});
|
||||
router.reload();
|
||||
}
|
||||
}}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
|
||||
<Button
|
||||
onButtonPressed={async () => {
|
||||
await deleteIntegration({
|
||||
integrationId: projectIntegration._id
|
||||
});
|
||||
router.reload();
|
||||
}}
|
||||
color="red"
|
||||
size="icon-md"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import NavHeader from "~/components/navigation/NavHeader";
|
||||
import Integration from "~/components/integrations/Integration";
|
||||
import FrameworkIntegrationSection from "~/components/integrations/FrameworkIntegrationSection";
|
||||
import CloudIntegrationSection from "~/components/integrations/CloudIntegrationSection";
|
||||
import IntegrationSection from "~/components/integrations/IntegrationSection";
|
||||
import frameworkIntegrationOptions from "../../public/json/frameworkIntegrations.json";
|
||||
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
|
||||
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
|
||||
import getIntegrationOptions from "../api/integrations/GetIntegrationOptions";
|
||||
import getBot from "../api/bot/getBot";
|
||||
import setBotActiveStatus from "../api/bot/setBotActiveStatus";
|
||||
import getLatestFileKey from "../api/workspace/getLatestFileKey";
|
||||
import ActivateBotDialog from "~/components/basic/dialog/ActivateBotDialog";
|
||||
import IntegrationAccessTokenDialog from "~/components/basic/dialog/IntegrationAccessTokenDialog";
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} = require('../../components/utilities/cryptography/crypto');
|
||||
const crypto = require("crypto");
|
||||
|
||||
export default function Integrations() {
|
||||
const [integrations, setIntegrations] = useState();
|
||||
const [projectIntegrations, setProjectIntegrations] = useState();
|
||||
const [authorizations, setAuthorizations] = useState();
|
||||
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
|
||||
const [integrationAuths, setIntegrationAuths] = useState([]);
|
||||
const [integrations, setIntegrations] = useState([]);
|
||||
const [bot, setBot] = useState(null);
|
||||
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
|
||||
// const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(true);
|
||||
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null);
|
||||
|
||||
const router = useRouter();
|
||||
const [csrfToken, setCsrfToken] = useState('');
|
||||
|
||||
useEffect(async () => {
|
||||
const tempCSRFToken = crypto.randomBytes(16).toString('hex');
|
||||
setCsrfToken(tempCSRFToken);
|
||||
localStorage.setItem('latestCSRFToken', tempCSRFToken);
|
||||
|
||||
let projectAuthorizations = await getWorkspaceAuthorizations({
|
||||
workspaceId: router.query.id
|
||||
});
|
||||
setAuthorizations(projectAuthorizations);
|
||||
|
||||
const projectIntegrations = await getWorkspaceIntegrations({
|
||||
workspaceId: router.query.id
|
||||
});
|
||||
setProjectIntegrations(projectIntegrations);
|
||||
|
||||
try {
|
||||
const integrationsData = await getIntegrations();
|
||||
setIntegrations(integrationsData);
|
||||
} catch (error) {
|
||||
console.log('Error', error);
|
||||
// get cloud integration options
|
||||
setCloudIntegrationOptions(
|
||||
await getIntegrationOptions()
|
||||
);
|
||||
|
||||
// get project integration authorizations
|
||||
setIntegrationAuths(
|
||||
await getWorkspaceAuthorizations({
|
||||
workspaceId: router.query.id,
|
||||
})
|
||||
);
|
||||
|
||||
// get project integrations
|
||||
setIntegrations(
|
||||
await getWorkspaceIntegrations({
|
||||
workspaceId: router.query.id,
|
||||
})
|
||||
);
|
||||
|
||||
// get project bot
|
||||
setBot(
|
||||
await getBot({
|
||||
workspaceId: router.query.id
|
||||
}
|
||||
));
|
||||
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return integrations ? (
|
||||
/**
|
||||
* 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: router.query.id });
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
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: bot.isActive ? false : true,
|
||||
botKey
|
||||
})).bot);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start integration for a given integration option [integrationOption]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.integrationOption - an integration option
|
||||
* @param {String} obj.name
|
||||
* @param {String} obj.type
|
||||
* @param {String} obj.docsLink
|
||||
* @returns
|
||||
*/
|
||||
const handleIntegrationOption = async ({ integrationOption }) => {
|
||||
|
||||
try {
|
||||
// generate CSRF token for OAuth2 code-token exchange integrations
|
||||
const state = crypto.randomBytes(16).toString("hex");
|
||||
localStorage.setItem('latestCSRFToken', state);
|
||||
|
||||
switch (integrationOption.name) {
|
||||
case 'Heroku':
|
||||
window.location = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
|
||||
break;
|
||||
case 'Vercel':
|
||||
window.location = `https://vercel.com/integrations/infisical-dev/new?state=${state}`;
|
||||
break;
|
||||
case 'Netlify':
|
||||
window.location = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`;
|
||||
break;
|
||||
// case 'Fly.io':
|
||||
// console.log('fly.io');
|
||||
// setIntegrationAccessTokenDialogOpen(true);
|
||||
// break;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open dialog to activate bot if bot is not active.
|
||||
* Otherwise, start integration [integrationOption]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.integrationOption - an integration option
|
||||
* @param {String} obj.name
|
||||
* @param {String} obj.type
|
||||
* @param {String} obj.docsLink
|
||||
* @returns
|
||||
*/
|
||||
const integrationOptionPress = ({ integrationOption }) => {
|
||||
try {
|
||||
if (bot.isActive) {
|
||||
// case: bot is active -> proceed with integration
|
||||
handleIntegrationOption({ integrationOption });
|
||||
return;
|
||||
}
|
||||
|
||||
// case: bot is not active -> open modal to activate bot
|
||||
setIsActivateBotDialogOpen(true);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<Head>
|
||||
<title>Dashboard</title>
|
||||
@@ -205,200 +184,41 @@ export default function Integrations() {
|
||||
content="Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files."
|
||||
/>
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full max-h-96 pb-2 h-screen max-h-[calc(100vh-10px)] overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||
<NavHeader pageName="Project Integrations" isProjectRelated={true} />
|
||||
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
|
||||
<div className="flex flex-row justify-start items-center text-3xl">
|
||||
<p className="font-semibold mr-4">Current Project Integrations</p>
|
||||
</div>
|
||||
<p className="mr-4 text-base text-gray-400">
|
||||
Manage your integrations of Infisical with third-party services.
|
||||
</p>
|
||||
</div>
|
||||
{projectIntegrations.length > 0 ? (
|
||||
projectIntegrations.map((projectIntegration) => (
|
||||
<Integration
|
||||
key={guidGenerator()}
|
||||
projectIntegration={projectIntegration}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col max-w-5xl justify-center bg-white/5 p-6 rounded-md mx-6 mt-8">
|
||||
<div className="relative px-4 flex flex-col text-gray-400 items-center justify-center">
|
||||
<div className="mb-1">
|
||||
You {"don't"} have any integrations set up yet. When you do,
|
||||
they will appear here.
|
||||
</div>
|
||||
<div className="">
|
||||
To start, click on any of the options below. It takes 5 clicks
|
||||
to set up.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2">
|
||||
<div className="flex flex-row justify-start items-center text-3xl">
|
||||
<p className="font-semibold mr-4">
|
||||
Platform & Cloud Integrations
|
||||
</p>
|
||||
</div>
|
||||
<p className="mr-4 text-base text-gray-400">
|
||||
Click on the itegration you want to connect. This will let your
|
||||
environment variables flow automatically into selected third-party
|
||||
services.
|
||||
</p>
|
||||
<p className="mr-4 text-xs text-gray-600 mt-1">
|
||||
Note: during an integration with Heroku, for security reasons, it
|
||||
is impossible to maintain end-to-end encryption. In theory, this
|
||||
lets Infisical decrypt yor environment variables. In practice, we
|
||||
can assure you that this will never be done, and it allows us to
|
||||
protect your secrets from bad actors online. The core Infisical
|
||||
service will always stay end-to-end encrypted. With any questions,
|
||||
reach out support@infisical.com.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 grid-cols-4 grid-rows-2 mx-6 mt-4 max-w-5xl">
|
||||
{Object.keys(integrations).map((integration) => (
|
||||
<div
|
||||
className={`relative ${
|
||||
['Heroku'].includes(integrations[integration].name)
|
||||
? ''
|
||||
: 'opacity-50'
|
||||
}`}
|
||||
key={integrations[integration].name}
|
||||
>
|
||||
<a
|
||||
href={`${
|
||||
['Heroku'].includes(integrations[integration].name)
|
||||
? `https://id.heroku.com/oauth/authorize?client_id=bc132901-935a-4590-b010-f1857efc380d&response_type=code&scope=write-protected&state=${csrfToken}`
|
||||
: '#'
|
||||
}`}
|
||||
rel="noopener"
|
||||
className={`relative flex flex-row bg-white/5 h-32 rounded-md p-4 items-center ${
|
||||
['Heroku'].includes(integrations[integration].name)
|
||||
? 'hover:bg-white/10 duration-200 cursor-pointer'
|
||||
: 'cursor-default grayscale'
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={`/images/integrations/${integrations[integration].name}.png`}
|
||||
height={70}
|
||||
width={70}
|
||||
alt="integration logo"
|
||||
></Image>
|
||||
{integrations[integration].name.split(' ').length > 2 ? (
|
||||
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs">
|
||||
<div>{integrations[integration].name.split(' ')[0]}</div>
|
||||
<div className="text-base">
|
||||
{integrations[integration].name.split(' ')[1]}{' '}
|
||||
{integrations[integration].name.split(' ')[2]}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs">
|
||||
{integrations[integration].name}
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
{['Heroku'].includes(integrations[integration].name) &&
|
||||
authorizations
|
||||
.map((authorization) => authorization.integration)
|
||||
.includes(integrations[integration].name.toLowerCase()) && (
|
||||
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
||||
<div
|
||||
onClick={() => {
|
||||
deleteIntegrationAuth({
|
||||
integrationAuthId: authorizations
|
||||
.filter(
|
||||
(authorization) =>
|
||||
authorization.integration ==
|
||||
integrations[integration].name.toLowerCase()
|
||||
)
|
||||
.map((authorization) => authorization._id)[0]
|
||||
});
|
||||
router.reload();
|
||||
}}
|
||||
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"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faX}
|
||||
className="text-xs mr-2 py-px"
|
||||
/>
|
||||
Revoke
|
||||
</div>
|
||||
<div className="w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className="text-xs mr-2"
|
||||
/>
|
||||
Authorized
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!['Heroku'].includes(integrations[integration].name) && (
|
||||
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
||||
<div className="w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90">
|
||||
Coming Soon
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2">
|
||||
<div className="flex flex-row justify-start items-center text-3xl">
|
||||
<p className="font-semibold mr-4">Framework Integrations</p>
|
||||
</div>
|
||||
<p className="mr-4 text-base text-gray-400">
|
||||
Click on a framework to get the setup instructions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 grid-cols-7 grid-rows-2 mx-6 mt-4 max-w-5xl">
|
||||
{frameworks.map((framework) => (
|
||||
<div key={framework.name}>
|
||||
<a
|
||||
href={framework.link}
|
||||
rel="noopener"
|
||||
className={`relative flex flex-row items-center justify-center bg-bunker-500 hover:bg-gradient-to-tr hover:from-sky-400 hover:to-primary duration-200 h-32 rounded-md p-0.5 items-center cursor-pointer`}
|
||||
>
|
||||
<div
|
||||
className={`font-semibold bg-bunker-500 flex flex-col items-center justify-center h-full w-full rounded-md text-gray-300 group-hover:text-gray-200 duration-200 ${
|
||||
framework?.name?.split(' ').length > 1
|
||||
? 'text-sm px-1'
|
||||
: 'text-xl px-2'
|
||||
} text-center w-full max-w-xs`}
|
||||
>
|
||||
{framework?.image && (
|
||||
<Image
|
||||
src={`/images/integrations/${framework.image}.png`}
|
||||
height={framework?.name ? 60 : 90}
|
||||
width={framework?.name ? 60 : 90}
|
||||
alt="integration logo"
|
||||
></Image>
|
||||
)}
|
||||
{framework?.name && framework?.image && (
|
||||
<div className="h-2"></div>
|
||||
)}
|
||||
{framework?.name && framework.name}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full max-h-96 pb-2 h-screen max-h-[calc(100vh-10px)] overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||
<NavHeader
|
||||
pageName="Project Integrations"
|
||||
isProjectRelated={true}
|
||||
/>
|
||||
<ActivateBotDialog
|
||||
isOpen={isActivateBotDialogOpen}
|
||||
closeModal={() => setIsActivateBotDialogOpen(false)}
|
||||
selectedIntegrationOption={selectedIntegrationOption}
|
||||
handleBotActivate={handleBotActivate}
|
||||
handleIntegrationOption={handleIntegrationOption}
|
||||
/>
|
||||
{/* <IntegrationAccessTokenDialog
|
||||
isOpen={isIntegrationAccessTokenDialogOpen}
|
||||
closeModal={() => setIntegrationAccessTokenDialogOpen(false)}
|
||||
selectedIntegrationOption={selectedIntegrationOption}
|
||||
handleBotActivate={handleBotActivate}
|
||||
handleIntegrationOption={handleIntegrationOption}
|
||||
/> */}
|
||||
<IntegrationSection integrations={integrations} />
|
||||
{cloudIntegrationOptions.length > 0 ? (
|
||||
<CloudIntegrationSection
|
||||
cloudIntegrationOptions={cloudIntegrationOptions}
|
||||
setSelectedIntegrationOption={setSelectedIntegrationOption}
|
||||
integrationOptionPress={integrationOptionPress}
|
||||
integrationAuths={integrationAuths}
|
||||
/>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
<FrameworkIntegrationSection
|
||||
frameworks={frameworkIntegrationOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center">
|
||||
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full"></div>
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
width={120}
|
||||
alt="loading animation"
|
||||
></Image>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
const queryString = require("query-string");
|
||||
import AuthorizeIntegration from "./api/integrations/authorizeIntegration";
|
||||
|
||||
export default function Netlify() {
|
||||
const router = useRouter();
|
||||
const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
|
||||
const code = parsedUrl.code;
|
||||
const state = parsedUrl.state;
|
||||
// modify comment here
|
||||
|
||||
/**
|
||||
* Here we forward to the default workspace if a user opens this url
|
||||
*/
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(async () => {
|
||||
if (state === localStorage.getItem('latestCSRFToken')) {
|
||||
localStorage.removeItem('latestCSRFToken');
|
||||
|
||||
await AuthorizeIntegration({
|
||||
workspaceId: localStorage.getItem('projectData.id'),
|
||||
code,
|
||||
integration: "netlify"
|
||||
});
|
||||
|
||||
router.push("/integrations/" + localStorage.getItem("projectData.id"));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
} catch (err) {
|
||||
console.error('Netlify integration error: ', err);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
Netlify.requireAuth = true;
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
const queryString = require("query-string");
|
||||
import AuthorizeIntegration from "./api/integrations/authorizeIntegration";
|
||||
|
||||
export default function Vercel() {
|
||||
const router = useRouter();
|
||||
const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
|
||||
const code = parsedUrl.code;
|
||||
const state = parsedUrl.state
|
||||
|
||||
/**
|
||||
* Here we forward to the default workspace if a user opens this url
|
||||
*/
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(async () => {
|
||||
if (state === localStorage.getItem('latestCSRFToken')) {
|
||||
localStorage.removeItem('latestCSRFToken');
|
||||
|
||||
await AuthorizeIntegration({
|
||||
workspaceId: localStorage.getItem('projectData.id'),
|
||||
code,
|
||||
integration: "vercel"
|
||||
});
|
||||
|
||||
router.push("/integrations/" + localStorage.getItem("projectData.id"));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
} catch (err) {
|
||||
console.error('Vercel integration error: ', err);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
Vercel.requireAuth = true;
|
||||
@@ -12,71 +12,20 @@ const reverseEnvMapping = {
|
||||
test: "Testing",
|
||||
};
|
||||
|
||||
const frameworks = [{
|
||||
"name": "Docker",
|
||||
"image": "Docker",
|
||||
"link": "https://infisical.com/docs/integrations/platforms/docker"
|
||||
}, {
|
||||
"name": "Docker Compose",
|
||||
"image": "Docker Compose",
|
||||
"link": "https://infisical.com/docs/integrations/platforms/docker-compose"
|
||||
}, {
|
||||
"name": "React",
|
||||
"image": "React",
|
||||
"link": "https://infisical.com/docs/integrations/frameworks/react"
|
||||
}, {
|
||||
"name": "Vue",
|
||||
"image": "Vue",
|
||||
"link": "https://infisical.com/docs/integrations/frameworks/vue"
|
||||
}, {
|
||||
"image": "Express",
|
||||
"link": "https://infisical.com/docs/integrations/frameworks/express"
|
||||
},{
|
||||
"image": "Next.js",
|
||||
"link": "https://infisical.com/docs/integrations/frameworks/nextjs"
|
||||
}, {
|
||||
"name": "Django",
|
||||
"image": "Django",
|
||||
"link": "https://infisical.com/docs/integrations/frameworks/django"
|
||||
}, {
|
||||
"name": "NestJS",
|
||||
"image": "NestJS",
|
||||
"link": "https://infisical.com/docs/integrations/frameworks/nestjs"
|
||||
}, {
|
||||
"name": "Nuxt",
|
||||
"image": "Nuxt",
|
||||
"link": "https://infisical.com/docs/integrations/frameworks/nuxt"
|
||||
}, {
|
||||
"name": "Gatsby",
|
||||
"image": "Gatsby",
|
||||
"link": "https://infisical.com/docs/integrations/frameworks/gatsby"
|
||||
}, {
|
||||
"name": "Remix",
|
||||
"image": "Remix",
|
||||
"link": "https://infisical.com/docs/integrations/frameworks/remix"
|
||||
}, {
|
||||
"name": "Vite",
|
||||
"image": "Vite",
|
||||
"link": "https://infisical.com/docs/integrations/frameworks/vite"
|
||||
}, {
|
||||
"image": "Fiber",
|
||||
"link": "https://infisical.com/docs/integrations/frameworks/fiber"
|
||||
}, {
|
||||
"name": "Flask",
|
||||
"image": "Flask",
|
||||
"link": "https://infisical.com/docs/integrations/frameworks/flask"
|
||||
}, {
|
||||
"name": "Laravel",
|
||||
"image": "Laravel",
|
||||
"link": "https://infisical.com/docs/integrations/frameworks/laravel"
|
||||
}, {
|
||||
"image": "Rails",
|
||||
"link": "https://infisical.com/docs/integrations/frameworks/rails"
|
||||
}
|
||||
]
|
||||
const vercelMapping = {
|
||||
|
||||
}
|
||||
|
||||
const reverseContextNetlifyMapping = {
|
||||
"All": "all",
|
||||
"Local development": "dev",
|
||||
"Branch deploys": "branch-deploy",
|
||||
"Deploy Previews": "deploy-preview",
|
||||
"Production": "production"
|
||||
}
|
||||
|
||||
export {
|
||||
envMapping,
|
||||
frameworks,
|
||||
reverseEnvMapping
|
||||
reverseEnvMapping,
|
||||
reverseContextNetlifyMapping
|
||||
};
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,98 @@
|
||||
[
|
||||
{
|
||||
"name": "Docker",
|
||||
"slug": "docker",
|
||||
"image": "Docker",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/docker"
|
||||
},
|
||||
{
|
||||
"name": "Docker Compose",
|
||||
"slug": "docker-compose",
|
||||
"image": "Docker Compose",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/docker-compose"
|
||||
},
|
||||
{
|
||||
"name": "React",
|
||||
"slug": "react",
|
||||
"image": "React",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/react"
|
||||
},
|
||||
{
|
||||
"name": "Vue",
|
||||
"slug": "vue",
|
||||
"image": "Vue",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/vue"
|
||||
},
|
||||
{
|
||||
"name": "Express",
|
||||
"slug": "express",
|
||||
"image": "Express",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/express"
|
||||
},
|
||||
{
|
||||
"name": "Next.js",
|
||||
"slug": "nextjs",
|
||||
"image": "Next.js",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/nextjs"
|
||||
},
|
||||
{
|
||||
"name": "Django",
|
||||
"slug": "django",
|
||||
"image": "Django",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/django"
|
||||
},
|
||||
{
|
||||
"name": "NestJS",
|
||||
"slug": "nestjs",
|
||||
"image": "NestJS",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/nestjs"
|
||||
},
|
||||
{
|
||||
"name": "Nuxt",
|
||||
"slug": "nuxt",
|
||||
"image": "Nuxt",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/nuxt"
|
||||
},
|
||||
{
|
||||
"name": "Gatsby",
|
||||
"slug": "gatsby",
|
||||
"image": "Gatsby",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/gatsby"
|
||||
},
|
||||
{
|
||||
"name": "Remix",
|
||||
"slug": "remix",
|
||||
"image": "Remix",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/remix"
|
||||
},
|
||||
{
|
||||
"name": "Vite",
|
||||
"slug": "vite",
|
||||
"image": "Vite",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/vite"
|
||||
},
|
||||
{
|
||||
"name": "Fiber",
|
||||
"slug": "fiber",
|
||||
"image": "Fiber",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/fiber"
|
||||
},
|
||||
{
|
||||
"name": "Flask",
|
||||
"slug": "flask",
|
||||
"image": "Flask",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/flask"
|
||||
},
|
||||
{
|
||||
"name": "Laravel",
|
||||
"slug": "laravel",
|
||||
"image": "Laravel",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/laravel"
|
||||
},
|
||||
{
|
||||
"name": "Rails",
|
||||
"slug": "rails",
|
||||
"image": "Rails",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/rails"
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user