Merge branch 'bot'

This commit is contained in:
Tuan Dang
2022-12-18 17:34:18 -05:00
79 changed files with 4049 additions and 1084 deletions
@@ -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
};
+31
View File
@@ -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;
-23
View File
@@ -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);
+4 -3
View File
@@ -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
View File
@@ -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>
);
}
+43
View File
@@ -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;
+42
View File
@@ -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;
+13 -64
View File
@@ -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"
}
]